Server HTTP dari pustaka standar
net/http bukan mainan. Ia HTTP/1.1 dan HTTP/2 lengkap, dengan connection pooling, TLS, dan satu goroutine per permintaan — dan ia yang mendasari setiap framework Go yang akan kamu temui.
Intisari
http.ListenAndServecukup untuk memulai, tapi tidak layak produksi — ia tidak punya satu pun timeout.- Setiap permintaan berjalan di goroutine sendiri. Handler-mu otomatis paralel, termasuk bug-nya.
Handlerhanyalah interface satu method. Seluruh ekosistem Go berdiri di atasnya.- HTTP/2 aktif otomatis begitu TLS menyala — tanpa konfigurasi.
- Framework Go (chi, echo, gin) memakai tipe yang sama; berpindah antar keduanya bukan penulisan ulang.
Server minimum
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /halo", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte("halo\n"))
})
log.Fatal(http.ListenAndServe(":8080", mux))
}
Baris terakhir itu yang tidak boleh sampai produksi. http.ListenAndServe memakai
http.Server tanpa satu pun timeout: sebuah klien yang membuka koneksi lalu diam akan menahan
goroutine dan memori selamanya. Sepuluh ribu koneksi seperti itu — mudah dibuat siapa saja — cukup untuk
menjatuhkan servermu. Bentuk yang benar ada di materi Timeout & batas server.
Bentuk yang layak dipakai
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
}
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
Perhatikan pemeriksaan http.ErrServerClosed: itu error yang dikembalikan saat server dimatikan
dengan sengaja. Tanpa pengecualian itu, setiap graceful shutdown akan tercatat sebagai kegagalan.
Satu goroutine per permintaan
Klien A ──┐
Klien B ──┼──► Listener ──► goroutine A ──► handler
Klien C ──┘ ├─► goroutine B ──► handler
└─► goroutine C ──► handler
Server membuat goroutine baru untuk setiap koneksi yang masuk. Konsekuensinya langsung ke cara kamu menulis handler:
| Akibat | Artinya untuk kodemu |
|---|---|
| Handler berjalan paralel | Variabel level paket yang ditulis handler adalah data race (Fase 2) |
Tidak perlu go sendiri | Paralelisme antar permintaan sudah gratis |
| Struct handler dipakai bersama | Isinya hanya boleh dependensi yang aman dipakai bersamaan — *sql.DB, *slog.Logger, klien HTTP |
| Panic mematikan satu permintaan | Server memulihkannya, tapi pasang middleware sendiri (Fase 1) |
| Goroutine bocor = kebocoran per permintaan | Yang paling cepat menghabiskan memori container |
Handler sebagai struct berisi dependensi
type Handler struct {
produk *produk.Layanan
log *slog.Logger
tmpl *template.Template
}
func New(p *produk.Layanan, log *slog.Logger) *Handler {
return &Handler{produk: p, log: log}
}
func (h *Handler) Daftar(w http.ResponseWriter, r *http.Request) {
hasil, err := h.produk.Cari(r.Context(), r.URL.Query().Get("q"))
if err != nil {
h.tulisError(w, r, err)
return
}
h.tulisJSON(w, http.StatusOK, hasil)
}
Ini pengganti "dependency injection container" di Go. Tidak ada registry global, tidak ada
variabel paket, tidak ada init() yang membangun koneksi. Semua dependensi masuk lewat
konstruktor, dan tesnya tinggal mengoper versi palsu. Dibahas tuntas di Fase 5.
Menulis respons dengan benar
func tulisJSON(w http.ResponseWriter, status int, v any) {
buf, err := json.Marshal(v)
if err != nil {
http.Error(w, "gagal encode", http.StatusInternalServerError)
return
}
// Urutannya WAJIB: header dulu, status kedua, body terakhir.
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
w.Write(buf)
}
Kesalahan urutan adalah bug diam-diam nomor satu di handler Go. Header yang diset
setelah WriteHeader — atau setelah Write pertama — diabaikan tanpa
peringatan. Dan panggilan WriteHeader kedua mencetak
superfluous response.WriteHeader call ke log tapi tidak mengubah respons. Kalau statusmu
selalu 200 padahal kodenya menulis 404, ini penyebabnya.
Perhatikan juga contoh di atas meng-encode ke buffer dulu. json.NewEncoder(w).Encode
langsung ke ResponseWriter lebih hemat memori, tapi kalau encoding gagal di tengah, status 200
sudah terkirim dan kamu tidak bisa lagi mengubahnya jadi 500.
Membaca permintaan
r.Method // "GET"
r.URL.Path // "/produk/42"
r.PathValue("id") // parameter rute (Go 1.22+)
r.URL.Query().Get("q") // query string
r.Header.Get("Authorization") // header
r.Context() // context permintaan — SELALU teruskan
r.RemoteAddr // alamat peer — di belakang ALB ini alamat ALB
// Form dan body
r.ParseForm()
r.FormValue("nama")
io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) // selalu batasi
Di belakang load balancer, r.RemoteAddr bukan alamat pengguna — ia alamat ALB.
Alamat asli ada di header X-Forwarded-For, dan header itu bisa dipalsukan
kalau kamu mempercayainya mentah-mentah. Cara yang benar (rate limit per IP, audit log) dibahas di
Fase 6 dan Fase 10.
TLS dan HTTP/2
srv.ListenAndServeTLS("cert.pem", "key.pem") // HTTP/2 aktif otomatis
Begitu TLS menyala, negosiasi ALPN membuat klien yang mendukung HTTP/2 memakainya — tanpa konfigurasi tambahan. Di AWS (Fase 10), TLS biasanya diterminasi di ALB atau CloudFront, sehingga aplikasimu sendiri melayani HTTP polos di dalam VPC. Itu wajar dan aman selama lalu lintasnya tidak keluar dari VPC.
Kenapa pustaka standar sering sudah cukup
| Kebutuhan | Di net/http |
|---|---|
| Routing dengan metode & parameter | ServeMux sejak Go 1.22 |
| Middleware | Fungsi yang membungkus Handler |
| Berkas statis | http.FileServer + embed.FS |
| Template HTML | html/template, dengan escaping otomatis |
| Klien HTTP dengan pool | http.Client |
| Tes tanpa server | net/http/httptest |
| Graceful shutdown | srv.Shutdown(ctx) |
Yang tidak ada: pengikatan & validasi otomatis, dokumentasi OpenAPI, dan sekumpulan middleware
siap pakai. Fase 7 membandingkan framework yang menyediakannya — tapi semuanya dibangun di atas
http.Handler yang sama, jadi apa pun yang kamu pelajari di sini tetap terpakai.
Latihan: bangun server dengan dua endpoint: GET /sehat yang mengembalikan
{"status":"ok"} dan GET /lambat yang tidur 3 detik. Panggil /lambat
di satu terminal dan /sehat di terminal lain bersamaan — buktikan yang kedua tidak
menunggu yang pertama. Lalu tukar urutan w.WriteHeader(404) dan
w.Header().Set(...), dan lihat header-nya hilang.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.