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:#f3e5f5HTTP 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:#fff3e0func 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 padahttp.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 sepertigorilla/muxatauchi.
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:#fce4ecpackage 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:#e8f5e9Membuat 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.Serverdengan timeout eksplisit di produksi —ReadTimeout,WriteTimeout, danIdleTimeoutmelindungi server dari Slowloris attack dan resource exhaustion.- Buat
http.ServeMuxsendiri, jangan gunakanhttp.DefaultServeMuxglobal — lebih aman, bisa diuji, dan tidak berpotensi konflik dengan package lain.- Urutan penulisan response wajib dipatuhi:
Header().Set()→WriteHeader()→Write(). Header yang diset setelahWriteHeadertidak 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.DefaultClientdi produksi karena tidak punya timeout — buathttp.ClientdenganTimeoutyang eksplisit.- Middleware dijalankan dari luar ke dalam — urutan
Recovery → Logging → Auth → Handlermemastikan 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.MaxBytesReaderwajib 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 diServeMux— seringkali cukup tanpa router eksternal.- Health check endpoint (
/healthatau/healthz) adalah standar untuk deployment di container — Kubernetes dan load balancer menggunakannya untuk routing traffic.