← Back to all posts
2026-08-09 · MailAnvil Team

Kirim Email Transaksional dengan MailAnvil + Go — Panduan Lengkap

Go semakin populer di kalangan backend developer Indonesia — terutama di fintech, e-commerce, dan startup SaaS. Performa tinggi, binary kecil, dan concurrency model-nya bikin Go ideal untuk layanan yang butuh throughput tinggi.

Tapi ada satu hal yang bikin pusing: ngirim email dari Go app.

Kebanyakan library email Go terlalu low-level (raw SMTP) atau terlalu berat (dependency berat). Tutorial ini kasih kamu cara paling simpel kirim email transaksional dari Go — pakai MailAnvil API, tanpa dependency eksternal, cuma net/http + encoding/json.

Kenapa MailAnvil?

Sebelum masuk kode, beberapa alasan kenapa MailAnvil cocok buat Go developer Indonesia:

Prasyarat

Struktur Project

go-email-app/
├── main.go
├── email/
│   └── client.go
└── go.mod

Step 1: Email Client yang Reusable

Bikin file email/client.go:

package email

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

const (
    APIBase  = "https://api.mailanvil.com/v1"
    APITimeout = 15 * time.Second
)

// Client wraps MailAnvil send endpoint
type Client struct {
    APIKey     string
    HTTPClient *http.Client
}

// NewClient creates a MailAnvil client
func NewClient(apiKey string) *Client {
    return &Client{
        APIKey: apiKey,
        HTTPClient: &http.Client{
            Timeout: APITimeout,
        },
    }
}

// SendRequest is the POST body for /v1/send
type SendRequest struct {
    From     string   `json:"from"`
    To       []string `json:"to"`
    Subject  string   `json:"subject"`
    HTML     string   `json:"html,omitempty"`
    Text     string   `json:"text,omitempty"`
    ReplyTo  string   `json:"reply_to,omitempty"`
}

// SendResponse is returned on success (202)
type SendResponse struct {
    ID      string `json:"id"`
    Message string `json:"message"`
}

// ErrorResponse for non-2xx responses
type ErrorResponse struct {
    Error struct {
        Code    string `json:"code"`
        Message string `json:"message"`
    } `json:"error"`
}

// Send queues a transactional email
func (c *Client) Send(req *SendRequest) (*SendResponse, error) {
    body, err := json.Marshal(req)
    if err != nil {
        return nil, fmt.Errorf("marshal request: %w", err)
    }

    httpReq, err := http.NewRequest(
        http.MethodPost,
        APIBase+"/send",
        bytes.NewReader(body),
    )
    if err != nil {
        return nil, fmt.Errorf("create request: %w", err)
    }

    httpReq.Header.Set("Authorization", "Bearer "+c.APIKey)
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("Idempotency-Key", generateIdempotencyKey())

    resp, err := c.HTTPClient.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("send request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode == http.StatusAccepted {
        var result SendResponse
        if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
            return nil, fmt.Errorf("decode response: %w", err)
        }
        return &result, nil
    }

    var errResp ErrorResponse
    json.NewDecoder(resp.Body).Decode(&errResp)
    return nil, fmt.Errorf("API error %d: %s — %s",
        resp.StatusCode,
        errResp.Error.Code,
        errResp.Error.Message,
    )
}

// generateIdempotencyKey creates a unique key for safe retries
func generateIdempotencyKey() string {
    return fmt.Sprintf("go-%d", time.Now().UnixNano())
}

Step 2: Template Email Sederhana

Tambahin fungsi helper untuk template email yang sering dipakai:

package email

import "fmt"

// WelcomeEmail builds a welcome email
func WelcomeEmail(recipientEmail, recipientName, appName string) *SendRequest {
    return &SendRequest{
        From:    fmt.Sprintf("%s <[email protected]>", appName),
        To:      []string{recipientEmail},
        Subject: fmt.Sprintf("Selamat datang di %s!", appName),
        HTML: fmt.Sprintf(`<!DOCTYPE html>
<html>
<body style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px; background: #000; color: #fff;">
  <div style="text-align: center; padding: 40px 0;">
    <h1 style="color: #ff801f;">Selamat datang, %s! 🎉</h1>
    <p style="font-size: 16px; color: #aaa;">Akun kamu di %s sudah aktif.</p>
    <p style="font-size: 14px; color: #666;">Mulai kirim email transaksional sekarang.</p>
    <a href="https://app.mailanvil.com" style="display: inline-block; padding: 12px 32px; background: #ff801f; color: #000; text-decoration: none; border-radius: 9999px; font-weight: 600; margin-top: 20px;">Ke Dashboard →</a>
  </div>
  <p style="font-size: 12px; color: #444; text-align: center; margin-top: 40px;">Dikirim oleh %s via MailAnvil</p>
</body>
</html>`, recipientName, appName, appName),
        ReplyTo: fmt.Sprintf("[email protected]"),
    }
}

// PasswordResetEmail builds a password reset email
func PasswordResetEmail(recipientEmail, resetLink string) *SendRequest {
    return &SendRequest{
        From:    "Security <[email protected]>",
        To:      []string{recipientEmail},
        Subject: "Reset Password Kamu",
        HTML: fmt.Sprintf(`<!DOCTYPE html>
<html>
<body style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px; background: #000; color: #fff;">
  <h1 style="color: #ff801f;">Reset Password</h1>
  <p style="color: #aaa;">Klik tombol di bawah untuk reset password kamu. Link berlaku 1 jam.</p>
  <a href="%s" style="display: inline-block; padding: 12px 32px; background: #ff801f; color: #000; text-decoration: none; border-radius: 9999px; font-weight: 600; margin: 20px 0;">Reset Password →</a>
  <p style="font-size: 12px; color: #444;">Kalau kamu tidak minta reset password, abaikan email ini.</p>
</body>
</html>`, resetLink),
    }
}

// InvoiceEmail builds a payment invoice notification
func InvoiceEmail(recipientEmail, recipientName, invoiceNo, amount, paymentLink string) *SendRequest {
    return &SendRequest{
        From:    "Billing <[email protected]>",
        To:      []string{recipientEmail},
        Subject: fmt.Sprintf("Invoice #%s — %s", invoiceNo, amount),
        HTML: fmt.Sprintf(`<!DOCTYPE html>
<html>
<body style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px; background: #000; color: #fff;">
  <h1 style="color: #ff801f;">Invoice Baru</h1>
  <p style="color: #aaa;">Hai %s,</p>
  <div style="background: #111; padding: 24px; border-radius: 12px; border: 1px solid rgba(214,235,253,0.19); margin: 20px 0;">
    <p style="margin: 0; color: #888;">Nomor Invoice</p>
    <p style="margin: 4px 0 16px; font-size: 18px; font-weight: 600;">#%s</p>
    <p style="margin: 0; color: #888;">Total</p>
    <p style="margin: 4px 0; font-size: 24px; font-weight: 700; color: #ff801f;">%s</p>
  </div>
  <a href="%s" style="display: inline-block; padding: 12px 32px; background: #ff801f; color: #000; text-decoration: none; border-radius: 9999px; font-weight: 600;">Bayar Sekarang →</a>
</body>
</html>`, recipientName, invoiceNo, amount, paymentLink),
    }
}

Step 3: Main Program

main.go:

package main

import (
    "log"
    "os"

    "go-email-app/email"
)

func main() {
    apiKey := os.Getenv("MAILANVIL_API_KEY")
    if apiKey == "" {
        log.Fatal("MAILANVIL_API_KEY environment variable required")
    }

    client := email.NewClient(apiKey)

    // Kirim welcome email
    resp, err := client.Send(email.WelcomeEmail(
        "[email protected]",
        "Budi",
        "WarungOnline",
    ))
    if err != nil {
        log.Fatalf("send failed: %v", err)
    }
    log.Printf("Email queued: %s — %s", resp.ID, resp.Message)

    // Kirim invoice
    resp, err = client.Send(email.InvoiceEmail(
        "[email protected]",
        "Budi",
        "INV-2026-0891",
        "Rp 149.000",
        "https://warungonline.com/bayar/inv-0891",
    ))
    if err != nil {
        log.Fatalf("invoice send failed: %v", err)
    }
    log.Printf("Invoice queued: %s", resp.ID)
}

Jalankan:

export MAILANVIL_API_KEY="re_..."
go run main.go

Output:

2026/08/09 14:30:00 Email queued: em_01J... — Email queued for delivery
2026/08/09 14:30:00 Invoice queued: em_01J...

Kenapa Gak Pakai SMTP?

Banyak Go developer yang default ke net/smtp bawaan. Masalahnya:

  1. Blocking I/Onet/smtp blocking di goroutine, gak cocok buat high-concurrency Go server
  2. Retry logic manual — kamu harus implement sendiri exponential backoff
  3. DKIM/SPF/DMARC sendiri — setup DNS rumit, gampang kena spam
  4. No monitoring — gak ada dashboard bounce/complaint

Dengan MailAnvil REST API, kamu dapet semua itu tanpa tambahan kode.

Production Tips

1. Idempotency Key untuk Safe Retry

Contoh di atas sudah include Idempotency-Key header. Ini ngejamin email gak terkirim dua kali kalau kamu retry request yang sama — penting banget untuk skenario payment notification.

2. Goroutine untuk Fire-and-Forget

Jangan blocking HTTP handler kamu:

func handleSignup(w http.ResponseWriter, r *http.Request) {
    // ... simpan user ke DB ...

    // Kirim welcome email async
    go func() {
        if _, err := emailClient.Send(email.WelcomeEmail(
            user.Email, user.Name, "MyApp",
        )); err != nil {
            log.Printf("welcome email failed: %v", err)
            // Kirim ke retry queue atau log untuk retry manual
        }
    }()

    w.WriteHeader(http.StatusCreated)
}

3. Graceful Shutdown

Pastiin email yang lagi dikirim gak kepotong saat server shutdown:

var wg sync.WaitGroup

func handleSignup(w http.ResponseWriter, r *http.Request) {
    wg.Add(1)
    go func() {
        defer wg.Done()
        emailClient.Send(...)
    }()
    w.WriteHeader(http.StatusCreated)
}

// Di main():
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
    <-sigCh
    log.Println("Shutting down... waiting for emails to finish")
    wg.Wait()
    os.Exit(0)
}()

Perbandingan dengan Library Lain

Approach Deps Concurrency Retry Harga IDR
net/smtp 0 (stdlib) Manual Manual Tergantung SMTP
gomail 2 deps OK Manual Tergantung SMTP
Resend Go SDK 1 dep OK Built-in USD + kurs
MailAnvil REST 0 (stdlib) Native goroutine Idempotency key IDR + QRIS

MailAnvil approach cuma butuh net/http + encoding/json — dua-duanya dari stdlib Go. Nol dependency eksternal. Binary kamu tetep kecil, build tetep cepet.

Kesimpulan

Go + MailAnvil = kombinasi minimalis yang powerful. Kamu dapet:

Coba sendiri: daftar di mailanvil.com, verifikasi domain, dan jalankan contoh kode di atas dalam 5 menit.

Kode lengkap tutorial ini: github.com/mailanvil/examples/go-email-client


Butuh API key? Daftar early access di mailanvil.com — gratis 500 email/bulan.