Net Http #

Package net/http adalah salah satu package paling powerful dalam standard library Go — ia menyediakan HTTP client dan server yang production-ready tanpa dependensi eksternal. Server Go bisa menangani puluhan ribu koneksi concurrent secara efisien karena setiap request dijalankan di goroutine tersendiri, dan net/http mengelola pool goroutine ini secara otomatis. Di sisi client, Go menyediakan http.Client yang lengkap dengan dukungan timeout, redirect, cookie, dan TLS. Memahami net/http dengan baik adalah fondasi dari hampir semua aplikasi Go yang berinteraksi dengan web — REST API, webhook, web scraper, proxy, dan microservice semuanya dibangun di atasnya.

Gambaran Besar Package net/http #

flowchart TD
    HTTP["package net/http"] --> Server["HTTP Server"]
    HTTP --> Client["HTTP Client"]

    Server --> S1["http.ListenAndServe\navvia port tertentu"]
    Server --> S2["http.ServeMux\nrouter bawaan"]
    Server --> S3["http.Handler interface\n{ServeHTTP(w, r)}"]
    Server --> S4["http.HandlerFunc\nfunc(w, r) sebagai Handler"]
    Server --> S5["http.Server struct\nkonfigurasi lengkap"]

    Client --> C1["http.Get / http.Post\nshortcut sederhana"]
    Client --> C2["http.Client struct\ntimeout, redirect, cookie"]
    Client --> C3["http.NewRequest\nrequest dengan kontrol penuh"]
    Client --> C4["http.Response\nstatus, header, body"]

    Server --> MW["Middleware Pattern\nchain of handlers"]
    Client --> TR["http.Transport\nkoneksi pool, TLS, proxy"]

    style HTTP fill:#4f86c6,color:#fff
    style Server fill:#e8f5e9
    style Client fill:#e3f2fd
    style MW fill:#fff3e0
    style TR fill:#f3e5f5

HTTP Server — Dasar #

Server HTTP paling sederhana di Go hanya butuh beberapa baris:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // Daftarkan handler untuk path tertentu
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Halo, Dunia!")
    })

    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprintln(w, "pong")
    })

    // Mulai server — blokir sampai error
    fmt.Println("Server berjalan di :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Println("Server error:", err)
    }
}

nil sebagai handler kedua di ListenAndServe berarti gunakan http.DefaultServeMux — router global yang diisi oleh http.HandleFunc. Untuk produksi, selalu buat ServeMux sendiri:

// ANTI-PATTERN: gunakan DefaultServeMux global
http.HandleFunc("/api/produk", handlerProduk) // mendaftarkan ke global mux

// BENAR: buat ServeMux sendiri — lebih aman, lebih mudah di-test
mux := http.NewServeMux()
mux.HandleFunc("/api/produk", handlerProduk)
mux.HandleFunc("/api/pengguna", handlerPengguna)

http.ListenAndServe(":8080", mux)

http.Handler Interface #

Semua handler di Go mengimplementasikan satu interface sederhana:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Ini berarti tipe apapun yang punya method ServeHTTP bisa digunakan sebagai handler — struct, fungsi yang dibungkus http.HandlerFunc, atau bahkan middleware chain:

// Cara 1: http.HandlerFunc — konversi fungsi biasa ke Handler
func handlerHalo(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Halo!")
}
mux.Handle("/halo", http.HandlerFunc(handlerHalo))
// atau lebih ringkas:
mux.HandleFunc("/halo", handlerHalo)

// Cara 2: struct yang mengimplementasikan Handler
type ProdukHandler struct {
    DB     *sql.DB
    Logger *log.Logger
}

func (h *ProdukHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Bisa akses h.DB dan h.Logger
    switch r.Method {
    case http.MethodGet:
        h.daftarProduk(w, r)
    case http.MethodPost:
        h.buatProduk(w, r)
    default:
        http.Error(w, "method tidak diizinkan", http.StatusMethodNotAllowed)
    }
}

// Daftarkan struct sebagai handler
produkHandler := &ProdukHandler{DB: db, Logger: logger}
mux.Handle("/api/produk", produkHandler)

ResponseWriter dan Request #

http.ResponseWriter dan *http.Request adalah dua parameter yang selalu ada di setiap handler. Memahami keduanya dengan baik adalah inti dari pengembangan HTTP di Go.

flowchart LR
    subgraph RW["http.ResponseWriter"]
        RW1["Header() http.Header\nset response header"]
        RW2["WriteHeader(statusCode int)\nkirim status code"]
        RW3["Write([]byte) (int, error)\nkirim body"]
    end

    subgraph Req["*http.Request"]
        Req1["Method — GET, POST, dll"]
        Req2["URL — path, query params"]
        Req3["Header — request headers"]
        Req4["Body io.ReadCloser — request body"]
        Req5["Context() — context request"]
        Req6["Form / PostForm — parsed form"]
        Req7["RemoteAddr — IP client"]
    end

    subgraph Order["Urutan Penulisan Response"]
        O1["1. Set Header (sebelum WriteHeader)"]
        O2["2. WriteHeader (status code)"]
        O3["3. Write (body)"]
        O1 --> O2 --> O3
    end

    style RW fill:#e8f5e9
    style Req fill:#e3f2fd
    style Order fill:#fff3e0
func handlerContoh(w http.ResponseWriter, r *http.Request) {
    // Baca informasi dari Request
    fmt.Println("Method:", r.Method)
    fmt.Println("Path:", r.URL.Path)
    fmt.Println("Query:", r.URL.Query().Get("nama"))
    fmt.Println("Header:", r.Header.Get("Authorization"))
    fmt.Println("Remote IP:", r.RemoteAddr)

    // PENTING: Header harus diset SEBELUM WriteHeader
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Request-ID", "abc123")

    // WriteHeader harus dipanggil SEBELUM Write
    // Jika tidak dipanggil, Write otomatis memanggil WriteHeader(200)
    w.WriteHeader(http.StatusCreated) // 201

    // Write body
    w.Write([]byte(`{"status":"sukses"}`))

    // ANTI-PATTERN: set header setelah WriteHeader — tidak berpengaruh!
    // w.Header().Set("X-Too-Late", "nilai ini tidak akan terkirim")
}

Membaca Request Body #

func handlerBuatProduk(w http.ResponseWriter, r *http.Request) {
    // Batasi ukuran body untuk mencegah serangan
    r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
    defer r.Body.Close() // selalu close body

    // Periksa Content-Type
    contentType := r.Header.Get("Content-Type")
    if !strings.HasPrefix(contentType, "application/json") {
        http.Error(w, "Content-Type harus application/json",
            http.StatusUnsupportedMediaType)
        return
    }

    var produk Produk
    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields()
    if err := decoder.Decode(&produk); err != nil {
        http.Error(w, "body tidak valid: "+err.Error(),
            http.StatusBadRequest)
        return
    }

    // Proses dan kirim response
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(produk)
}

Query Parameter dan Path #

func handlerCariProduk(w http.ResponseWriter, r *http.Request) {
    // Query parameters: /api/produk?q=laptop&kategori=elektronik&hal=2
    query := r.URL.Query()

    kata := query.Get("q")           // string, kosong jika tidak ada
    kategori := query.Get("kategori")
    halStr := query.Get("hal")

    hal := 1
    if halStr != "" {
        n, err := strconv.Atoi(halStr)
        if err != nil || n < 1 {
            http.Error(w, "parameter 'hal' tidak valid", http.StatusBadRequest)
            return
        }
        hal = n
    }

    fmt.Fprintf(w, "Cari: %q, Kategori: %q, Halaman: %d\n",
        kata, kategori, hal)
}

// Path parameter — Go 1.22+ mendukung pattern {id}
// Untuk Go versi lama, parsing manual atau pakai router eksternal
mux.HandleFunc("/api/produk/{id}", func(w http.ResponseWriter, r *http.Request) {
    // Go 1.22+
    id := r.PathValue("id")
    fmt.Fprintf(w, "Produk ID: %s\n", id)
})
Go 1.22 memperkenalkan peningkatan signifikan pada http.ServeMux: dukungan method matching (GET /api/produk) dan path parameter (/api/produk/{id}). Jika menggunakan Go 1.22+, fitur ini mengurangi kebutuhan akan router eksternal untuk banyak kasus. Untuk Go versi lama, parsing path dilakukan manual atau menggunakan library seperti gorilla/mux atau chi.

http.Server — Konfigurasi Produksi #

http.ListenAndServe adalah shortcut yang nyaman tapi tidak dikonfigurasi untuk produksi. Untuk aplikasi nyata, selalu gunakan http.Server dengan timeout yang eksplisit:

flowchart LR
    Client["HTTP Client"] --> Server["http.Server"]

    subgraph Timeouts["Timeout yang Penting"]
        T1["ReadTimeout\nwaktu total untuk baca request"]
        T2["ReadHeaderTimeout\nwaktu untuk baca header saja"]
        T3["WriteTimeout\nwaktu untuk tulis response"]
        T4["IdleTimeout\nwaktu koneksi idle di keep-alive"]
    end

    Server --> Timeouts

    subgraph Risk["Tanpa Timeout"]
        R1["Slowloris attack\nclient kirim header sangat lambat"]
        R2["Resource exhaustion\ngoroutine menumpuk tanpa batas"]
        R3["Memory leak\nkoneksi tidak pernah ditutup"]
    end

    style Timeouts fill:#e8f5e9
    style Risk fill:#fce4ec
package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    daftarkanRoute(mux)

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,

        // Timeout wajib untuk produksi
        ReadTimeout:       15 * time.Second,
        ReadHeaderTimeout: 5 * time.Second,
        WriteTimeout:      15 * time.Second,
        IdleTimeout:       60 * time.Second,

        // Batas ukuran header
        MaxHeaderBytes: 1 << 20, // 1 MB
    }

    // Jalankan server di goroutine terpisah
    go func() {
        fmt.Printf("Server berjalan di %s\n", server.Addr)
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            fmt.Fprintf(os.Stderr, "server error: %v\n", err)
            os.Exit(1)
        }
    }()

    // Tunggu sinyal shutdown
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    <-sigChan

    fmt.Println("\nMemulai graceful shutdown...")

    // Beri waktu 30 detik untuk request yang sedang berjalan selesai
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        fmt.Fprintf(os.Stderr, "shutdown error: %v\n", err)
    }
    fmt.Println("Server berhenti")
}

func daftarkanRoute(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/produk", handlerDaftarProduk)
    mux.HandleFunc("POST /api/produk", handlerBuatProduk)
    mux.HandleFunc("GET /api/produk/{id}", handlerDetailProduk)
    mux.HandleFunc("GET /health", handlerHealth)
}

Middleware Pattern #

Middleware adalah fungsi yang membungkus handler — menambahkan perilaku sebelum atau sesudah handler asli dijalankan. Ini adalah pola yang sangat umum untuk logging, autentikasi, CORS, rate limiting, dan recovery dari panic.

flowchart LR
    Req["Request"] --> MW1["Middleware 1\nLogging"] --> MW2["Middleware 2\nAuth"] --> MW3["Middleware 3\nRateLimit"] --> H["Handler\n(logika bisnis)"]
    H --> MW3b["Middleware 3\n(after)"] --> MW2b["Middleware 2\n(after)"] --> MW1b["Middleware 1\n(after)"] --> Resp["Response"]

    style MW1 fill:#e3f2fd
    style MW2 fill:#e8f5e9
    style MW3 fill:#fff3e0
    style H fill:#4f86c6,color:#fff
    style MW1b fill:#e3f2fd
    style MW2b fill:#e8f5e9
    style MW3b fill:#fff3e0
// Tipe middleware: fungsi yang menerima Handler dan mengembalikan Handler
type Middleware func(http.Handler) http.Handler

// Middleware logging — catat setiap request
func middlewareLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        mulai := time.Now()

        // Bungkus ResponseWriter untuk menangkap status code
        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        next.ServeHTTP(rw, r)

        fmt.Printf("[%s] %s %s %d %v\n",
            mulai.Format("2006-01-02 15:04:05"),
            r.Method,
            r.URL.Path,
            rw.statusCode,
            time.Since(mulai),
        )
    })
}

// ResponseWriter wrapper untuk menangkap status code
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// Middleware autentikasi — periksa Bearer token
func middlewareAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if !strings.HasPrefix(authHeader, "Bearer ") {
            http.Error(w, "autentikasi diperlukan", http.StatusUnauthorized)
            return
        }

        token := strings.TrimPrefix(authHeader, "Bearer ")
        pengguna, err := validasiToken(token)
        if err != nil {
            http.Error(w, "token tidak valid", http.StatusUnauthorized)
            return
        }

        // Simpan pengguna di context untuk diakses handler
        ctx := context.WithValue(r.Context(), keyPengguna, pengguna)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Middleware recovery dari panic
func middlewareRecovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                fmt.Fprintf(os.Stderr, "panic: %v\n", rec)
                http.Error(w, "internal server error",
                    http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// Menggabungkan middleware — urutan dari luar ke dalam
func chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

// Penggunaan
mux := http.NewServeMux()
mux.HandleFunc("GET /api/produk", handlerDaftarProduk)

// Recovery → Logging → Auth → Handler
handler := chain(mux,
    middlewareRecovery,
    middlewareLogging,
    middlewareAuth,
)

http.ListenAndServe(":8080", handler)

Menyimpan dan Membaca Nilai di Context #

// Definisikan key dengan tipe kustom untuk menghindari collision
type contextKey string

const (
    keyPengguna  contextKey = "pengguna"
    keyRequestID contextKey = "request_id"
)

// Simpan di middleware
ctx := context.WithValue(r.Context(), keyPengguna, pengguna)
r = r.WithContext(ctx)

// Baca di handler
func handlerProfil(w http.ResponseWriter, r *http.Request) {
    pengguna, ok := r.Context().Value(keyPengguna).(*Pengguna)
    if !ok || pengguna == nil {
        http.Error(w, "tidak terautentikasi", http.StatusUnauthorized)
        return
    }
    json.NewEncoder(w).Encode(pengguna)
}

HTTP Client #

Go menyediakan http.Client untuk membuat HTTP request ke server lain. Jangan gunakan http.DefaultClient di produksi karena tidak punya timeout:

flowchart TD
    subgraph Anti["ANTI-PATTERN: http.DefaultClient"]
        A1["http.Get(url)\natau http.DefaultClient.Get(url)"]
        A2["Tidak ada timeout!\nBisa blokir selamanya"]
        A1 --> A2
    end

    subgraph Good["BENAR: http.Client dengan timeout"]
        G1["client := &http.Client{\n  Timeout: 30*time.Second\n}"]
        G2["client.Get(url)"]
        G3["Otomatis cancel\nsetelah 30 detik"]
        G1 --> G2 --> G3
    end

    style Anti fill:#fce4ec
    style Good fill:#e8f5e9

Membuat HTTP Client #

import (
    "net/http"
    "time"
)

// Client untuk produksi — selalu set timeout
func buatHTTPClient() *http.Client {
    transport := &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
        TLSHandshakeTimeout: 10 * time.Second,
    }

    return &http.Client{
        Timeout:   30 * time.Second,
        Transport: transport,
    }
}

// Gunakan satu client untuk seluruh aplikasi (singleton)
var httpClient = buatHTTPClient()

GET Request #

func ambilDataPengguna(id int) (*Pengguna, error) {
    url := fmt.Sprintf("https://api.example.com/users/%d", id)

    resp, err := httpClient.Get(url)
    if err != nil {
        return nil, fmt.Errorf("ambilDataPengguna: request gagal: %w", err)
    }
    defer resp.Body.Close() // WAJIB: selalu close response body

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("ambilDataPengguna: status %d dari server",
            resp.StatusCode)
    }

    var pengguna Pengguna
    if err := json.NewDecoder(resp.Body).Decode(&pengguna); err != nil {
        return nil, fmt.Errorf("ambilDataPengguna: decode response: %w", err)
    }

    return &pengguna, nil
}

POST Request dengan JSON Body #

func kirimDataProduk(produk *Produk) error {
    body, err := json.Marshal(produk)
    if err != nil {
        return fmt.Errorf("kirimDataProduk: marshal: %w", err)
    }

    req, err := http.NewRequest(
        http.MethodPost,
        "https://api.example.com/products",
        bytes.NewReader(body),
    )
    if err != nil {
        return fmt.Errorf("kirimDataProduk: buat request: %w", err)
    }

    // Set header
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("X-Request-ID", uuid.New().String())

    resp, err := httpClient.Do(req)
    if err != nil {
        return fmt.Errorf("kirimDataProduk: kirim request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusCreated {
        // Baca body error untuk informasi lebih detail
        errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
        return fmt.Errorf("kirimDataProduk: server return %d: %s",
            resp.StatusCode, errBody)
    }

    return nil
}

Request dengan Context — Timeout dan Cancellation #

func ambilDataDenganTimeout(ctx context.Context, url string) ([]byte, error) {
    // Buat sub-context dengan timeout spesifik untuk request ini
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, fmt.Errorf("buat request: %w", err)
    }

    resp, err := httpClient.Do(req)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("request timeout setelah 10 detik")
        }
        return nil, fmt.Errorf("request gagal: %w", err)
    }
    defer resp.Body.Close()

    // Batasi ukuran response yang dibaca
    data, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) // 10 MB
    if err != nil {
        return nil, fmt.Errorf("baca response: %w", err)
    }

    return data, nil
}

Menangani Form dan File Upload #

// Form HTML biasa (application/x-www-form-urlencoded)
func handlerSubmitForm(w http.ResponseWriter, r *http.Request) {
    // ParseForm wajib dipanggil sebelum akses r.Form atau r.FormValue
    if err := r.ParseForm(); err != nil {
        http.Error(w, "gagal parse form", http.StatusBadRequest)
        return
    }

    nama := r.FormValue("nama")         // shortcut untuk r.Form.Get("nama")
    email := r.FormValue("email")
    umurStr := r.FormValue("umur")

    umur, err := strconv.Atoi(umurStr)
    if err != nil {
        http.Error(w, "umur tidak valid", http.StatusBadRequest)
        return
    }

    fmt.Fprintf(w, "Diterima: %s (%s), umur %d\n", nama, email, umur)
}

// Multipart form — untuk file upload
func handlerUploadFoto(w http.ResponseWriter, r *http.Request) {
    // Batasi ukuran: 10 MB untuk file, 32 MB total
    if err := r.ParseMultipartForm(32 << 20); err != nil {
        http.Error(w, "gagal parse multipart form", http.StatusBadRequest)
        return
    }

    // Ambil field teks biasa
    nama := r.FormValue("nama")

    // Ambil file
    file, header, err := r.FormFile("foto")
    if err != nil {
        http.Error(w, "file foto tidak ditemukan", http.StatusBadRequest)
        return
    }
    defer file.Close()

    // Validasi tipe file
    buffer := make([]byte, 512)
    _, err = file.Read(buffer)
    if err != nil {
        http.Error(w, "gagal baca file", http.StatusBadRequest)
        return
    }
    contentType := http.DetectContentType(buffer)
    if !strings.HasPrefix(contentType, "image/") {
        http.Error(w, "hanya file gambar yang diterima",
            http.StatusUnsupportedMediaType)
        return
    }

    // Reset posisi baca ke awal setelah deteksi content type
    file.Seek(0, 0)

    // Simpan file
    namaFile := fmt.Sprintf("upload/%s_%s", nama, header.Filename)
    dst, err := os.Create(namaFile)
    if err != nil {
        http.Error(w, "gagal simpan file", http.StatusInternalServerError)
        return
    }
    defer dst.Close()

    bytesDisalin, err := io.Copy(dst, file)
    if err != nil {
        http.Error(w, "gagal simpan file", http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "File %s (%d bytes) berhasil diupload\n",
        header.Filename, bytesDisalin)
}

Melayani File Statis #

// Melayani direktori file statis
mux.Handle("/static/",
    http.StripPrefix("/static/",
        http.FileServer(http.Dir("./assets"))))

// Melayani satu file
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "./assets/favicon.ico")
})

// Embed file ke dalam binary (Go 1.16+)
import "embed"

//go:embed assets/*
var assets embed.FS

mux.Handle("/static/",
    http.StripPrefix("/static/",
        http.FileServer(http.FS(assets))))

Pola Penggunaan di Produksi #

REST API Handler yang Lengkap #

type ProdukService interface {
    Daftar(ctx context.Context, filter Filter) ([]Produk, error)
    Cari(ctx context.Context, id int) (*Produk, error)
    Buat(ctx context.Context, input InputProduk) (*Produk, error)
    Perbarui(ctx context.Context, id int, input InputProduk) (*Produk, error)
    Hapus(ctx context.Context, id int) error
}

type ProdukHandlerV2 struct {
    Service ProdukService
}

func (h *ProdukHandlerV2) DaftarProduk(w http.ResponseWriter, r *http.Request) {
    produk, err := h.Service.Daftar(r.Context(), Filter{})
    if err != nil {
        kirimErrorJSON(w, http.StatusInternalServerError, "gagal memuat produk")
        return
    }
    kirimJSON(w, http.StatusOK, produk)
}

func (h *ProdukHandlerV2) DetailProduk(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id") // Go 1.22+
    id, err := strconv.Atoi(idStr)
    if err != nil {
        kirimErrorJSON(w, http.StatusBadRequest, "ID tidak valid")
        return
    }

    produk, err := h.Service.Cari(r.Context(), id)
    if err != nil {
        if errors.Is(err, ErrTidakDitemukan) {
            kirimErrorJSON(w, http.StatusNotFound, "produk tidak ditemukan")
            return
        }
        kirimErrorJSON(w, http.StatusInternalServerError, "gagal memuat produk")
        return
    }

    kirimJSON(w, http.StatusOK, produk)
}

func (h *ProdukHandlerV2) BuatProduk(w http.ResponseWriter, r *http.Request) {
    var input InputProduk
    if err := decodeJSON(r, &input); err != nil {
        kirimErrorJSON(w, http.StatusBadRequest, err.Error())
        return
    }

    produk, err := h.Service.Buat(r.Context(), input)
    if err != nil {
        var errVal *ErrValidasi
        if errors.As(err, &errVal) {
            kirimErrorJSON(w, http.StatusBadRequest, errVal.Pesan)
            return
        }
        kirimErrorJSON(w, http.StatusInternalServerError, "gagal membuat produk")
        return
    }

    kirimJSON(w, http.StatusCreated, produk)
}

// Helper functions
func kirimJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func kirimErrorJSON(w http.ResponseWriter, status int, pesan string) {
    kirimJSON(w, status, map[string]string{"error": pesan})
}

func decodeJSON(r *http.Request, target any) error {
    r.Body = http.MaxBytesReader(r.ResponseWriter, r.Body, 1<<20)
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    return dec.Decode(target)
}

// Registrasi route
func (h *ProdukHandlerV2) DaftarkanRoute(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/v1/produk", h.DaftarProduk)
    mux.HandleFunc("POST /api/v1/produk", h.BuatProduk)
    mux.HandleFunc("GET /api/v1/produk/{id}", h.DetailProduk)
}

HTTP Client dengan Retry #

import (
    "context"
    "math"
    "net/http"
    "time"
)

type RetryClient struct {
    client     *http.Client
    maxRetry   int
    baseDelay  time.Duration
}

func (rc *RetryClient) Do(req *http.Request) (*http.Response, error) {
    var lastErr error

    for attempt := 0; attempt <= rc.maxRetry; attempt++ {
        if attempt > 0 {
            // Backoff eksponensial: 1s, 2s, 4s, 8s, ...
            delay := time.Duration(math.Pow(2, float64(attempt-1))) *
                rc.baseDelay
            select {
            case <-time.After(delay):
            case <-req.Context().Done():
                return nil, req.Context().Err()
            }
        }

        // Clone request untuk retry (body sudah dibaca di attempt pertama)
        resp, err := rc.client.Do(req)
        if err != nil {
            lastErr = err
            // Retry hanya untuk error jaringan, bukan error aplikasi
            continue
        }

        // Retry untuk 5xx server error
        if resp.StatusCode >= 500 {
            resp.Body.Close()
            lastErr = fmt.Errorf("server error: %d", resp.StatusCode)
            continue
        }

        return resp, nil
    }

    return nil, fmt.Errorf("gagal setelah %d retry: %w", rc.maxRetry, lastErr)
}

Health Check Handler #

type HealthStatus struct {
    Status    string            `json:"status"`
    Waktu     time.Time         `json:"waktu"`
    Versi     string            `json:"versi"`
    Komponen  map[string]string `json:"komponen"`
}

func handlerHealth(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        status := HealthStatus{
            Waktu:    time.Now(),
            Versi:    "1.0.0",
            Komponen: make(map[string]string),
        }

        // Periksa koneksi database
        ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
        defer cancel()

        if err := db.PingContext(ctx); err != nil {
            status.Komponen["database"] = "down: " + err.Error()
            status.Status = "degraded"
        } else {
            status.Komponen["database"] = "up"
            status.Status = "ok"
        }

        httpStatus := http.StatusOK
        if status.Status != "ok" {
            httpStatus = http.StatusServiceUnavailable
        }

        kirimJSON(w, httpStatus, status)
    }
}

Kapan Beralih ke Alternatif #

Tetap gunakan net/http jika:
  ✓ HTTP server dan client untuk semua kebutuhan umum
  ✓ REST API dengan routing sederhana (terutama Go 1.22+)
  ✓ HTTP client untuk memanggil API eksternal
  ✓ Serving file statis
  ✓ Middleware chain untuk logging, auth, recovery

Pertimbangkan router eksternal jika:
  ✗ Go < 1.22 dan butuh path parameter (/api/produk/:id)
  ✗ Route grouping dengan prefix dan middleware per group
  ✗ Named route dan URL generation
  → chi, gorilla/mux, httprouter, echo, gin

Pertimbangkan framework jika:
  ✗ Butuh ekosistem lengkap: ORM, validasi, template, auth
  ✗ Tim lebih nyaman dengan konvensi framework
  → echo, gin, fiber (berbasis fasthttp, bukan net/http)

Pertimbangkan gRPC jika:
  ✗ Komunikasi antar microservice yang butuh performa tinggi
  ✗ Kontrak API yang ketat dengan Protocol Buffers
  ✗ Streaming bidirectional
  → google.golang.org/grpc

Ringkasan #

  • Selalu gunakan http.Server dengan timeout eksplisit di produksi — ReadTimeout, WriteTimeout, dan IdleTimeout melindungi server dari Slowloris attack dan resource exhaustion.
  • Buat http.ServeMux sendiri, jangan gunakan http.DefaultServeMux global — lebih aman, bisa diuji, dan tidak berpotensi konflik dengan package lain.
  • Urutan penulisan response wajib dipatuhi: Header().Set()WriteHeader()Write(). Header yang diset setelah WriteHeader tidak akan terkirim.
  • Selalu defer resp.Body.Close() setelah HTTP client request berhasil — body yang tidak ditutup menyebabkan koneksi tidak dikembalikan ke pool dan goroutine leak.
  • Jangan gunakan http.DefaultClient di produksi karena tidak punya timeout — buat http.Client dengan Timeout yang eksplisit.
  • Middleware dijalankan dari luar ke dalam — urutan Recovery → Logging → Auth → Handler memastikan semua request ter-log dan panic ter-recover, bahkan sebelum autentikasi diperiksa.
  • Gunakan context untuk timeout request dengan http.NewRequestWithContext — context yang di-cancel dari luar akan otomatis membatalkan HTTP request yang sedang berjalan.
  • http.MaxBytesReader wajib digunakan untuk membatasi ukuran request body — tanpanya, client bisa mengirim body tak terbatas dan menguras memori server.
  • Go 1.22+: gunakan method matching (GET /api/produk) dan path parameter (/api/produk/{id}) langsung di ServeMux — seringkali cukup tanpa router eksternal.
  • Health check endpoint (/health atau /healthz) adalah standar untuk deployment di container — Kubernetes dan load balancer menggunakannya untuk routing traffic.

← Sebelumnya: Encoding Json   Berikutnya: Context →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact