Go HTTP Dasar
Definisi singkat
net/http adalah paket standar Go untuk membuat HTTP server dan HTTP client. Untuk sisi server, paket ini memberi fondasi yang cukup lengkap untuk menerima request, membaca body, menulis response, dan mengatur routing sederhana tanpa dependency tambahan.
Kalau dipakai di project kecil atau API internal, net/http sering sudah cukup. Kamu baru butuh router atau middleware tambahan kalau kebutuhan routing mulai kompleks, ada banyak cross-cutting concern, atau struktur handler mulai sulit dirawat.
Bagaimana net/http bekerja
Di sisi server, alurnya sederhana:
- server mendengarkan port tertentu
- request masuk dari client
net/httpmembuat*http.Request- server mencari handler yang cocok
- handler menulis response lewat
http.ResponseWriter - response dikirim balik ke client
Yang penting dipahami: di Go, HTTP server tidak bergantung pada framework besar untuk bisa berjalan. Yang benar-benar dibutuhkan adalah handler yang memenuhi interface berikut:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}Artinya, setiap tipe yang punya method ServeHTTP(http.ResponseWriter, *http.Request) bisa dipakai sebagai handler.
Contoh server paling dasar
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
http.ListenAndServe(":8080", nil)
}Kalau ListenAndServe diberi nil, Go memakai http.DefaultServeMux sebagai router default.
Routing dasar
Routing adalah proses mencocokkan request path dan method ke handler yang tepat.
Opsi routing yang umum
1. http.HandleFunc dan DefaultServeMux
Cocok untuk:
- demo
- API kecil
- service internal sederhana
- proof of concept
Kelemahannya:
- kontrol routing terbatas
- mudah bercampur antar package kalau tidak disiplin
- sulit saat route makin banyak dan butuh grouping
2. http.NewServeMux
Ini pilihan yang lebih rapi daripada memakai default global.
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/users", usersHandler)
http.ListenAndServe(":8080", mux)Pakai NewServeMux kalau kamu ingin:
- menghindari state global
- lebih mudah dites
- setup server lebih eksplisit
Routing method-aware
ServeMux hanya cocokkan path, bukan method. Jadi kalau endpoint yang sama harus beda perilaku untuk GET, POST, atau DELETE, method check harus dilakukan di handler atau lewat wrapper.
func usersHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Write([]byte("list users"))
case http.MethodPost:
w.Write([]byte("create user"))
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}Kapan perlu router sendiri
Kamu mulai butuh router sendiri atau framework/router tambahan kalau:
- route banyak dan bertingkat
- butuh parameter path seperti
/users/{id} - butuh grouping middleware per subtree
- perlu method matching yang lebih ergonomis
- perlu 404/405 handling yang lebih terstruktur
Kalau masih bisa dibaca jelas dengan ServeMux, jangan buru-buru nambah abstraksi.
Alur request→router→middleware→handler→response
Urutan ini penting karena middleware biasanya dipakai untuk hal yang sifatnya lintas handler, misalnya:
- logging
- auth
- request ID
- timeout
- recovery dari panic
- CORS
- metrics
Kapan perlu handler sendiri
Kamu tidak harus menulis tipe handler sendiri untuk semua hal. http.HandlerFunc sudah cukup untuk banyak kasus.
Pakai HandlerFunc kalau
- logic-nya kecil
- tidak ada dependency state yang perlu disimpan
- endpoint berdiri sendiri
Pakai tipe handler sendiri kalau
- handler butuh dependency seperti DB, logger, service, atau config
- kamu ingin test handler tanpa global variable
- kamu ingin menaruh helper method di satu tipe
- handler punya state yang dipakai ulang
Contoh:
type UserHandler struct {
Logger *log.Logger
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Logger.Println("request masuk")
w.Write([]byte("users"))
}Pola ini membantu kalau project mulai membesar. Handlermu tetap kecil, dependency-nya jelas, dan testing lebih mudah.
Middleware pattern
Middleware adalah fungsi yang membungkus handler lain. Tujuannya bukan mengganti business logic, tapi menambahkan perilaku sebelum atau sesudah handler.
Bentuk dasar middleware
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// sebelum handler
next.ServeHTTP(w, r)
// sesudah handler
})
}Kenapa middleware berguna
Middleware cocok untuk logic yang:
- dipakai di banyak endpoint
- tidak spesifik ke satu domain
- harus konsisten di seluruh API
Contoh paling umum:
- auth check
- logging request/response
- panic recovery
- tracing
- rate limiting
- compress response
Contoh middleware logging
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}Middleware chain
Middleware bisa ditumpuk.
handler := Logging(Auth(mux))Urutan penting. Kalau auth harus terjadi sebelum handler, middleware auth harus ada di lapisan yang tepat.
Parsing JSON request
JSON API biasanya menerima payload lewat request body. Di Go, parsing JSON paling umum memakai encoding/json.
Langkah dasar
- batasi method
- cek
Content-Typekalau perlu - decode body ke struct
- validasi hasil decode
- handle error dengan status code yang tepat
Contoh request body struct
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}Contoh decode JSON
func createUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if req.Name == "" || req.Email == "" {
http.Error(w, "missing required field", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
}Hal yang perlu diperhatikan saat decode JSON
Decodemembaca dari body stream, bukan dari string yang sudah ditampung penuh- kalau body besar, kamu sebaiknya batasi ukurannya
- field yang tidak ada di struct akan diabaikan oleh default
- typo di tag JSON bisa bikin field diam-diam kosong
Batasi ukuran body
Ini penting supaya server kecil tidak gampang dipaksa membaca payload besar.
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MBMenulis JSON response
Untuk response JSON, pakai json.NewEncoder(w).Encode(...) supaya format konsisten.
Contoh response helper
type ErrorResponse struct {
Error string `json:"error"`
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}Contoh response sukses
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
writeJSON(w, http.StatusCreated, UserResponse{
ID: 1,
Name: req.Name,
Email: req.Email,
})Kenapa helper response penting
Helper kecil seperti ini mengurangi duplikasi dan menjaga perilaku response tetap konsisten:
- header
Content-Type - status code
- struktur error
- format output
Contoh API kecil yang rapi
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/users", usersHandler)
handler := Logging(mux)
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
if req.Name == "" || req.Email == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and email are required"})
return
}
writeJSON(w, http.StatusCreated, UserResponse{
ID: 1,
Name: req.Name,
Email: req.Email,
})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}Jebakan umum pada server Go kecil
1. Menggunakan http.DefaultServeMux tanpa sadar
Kalau handler didaftarkan ke fungsi global seperti http.HandleFunc, state routing jadi global. Ini bisa bikin testing dan struktur package berantakan.
Lebih aman gunakan http.NewServeMux() lalu inject ke http.Server.
2. Lupa cek method
ServeMux tidak membedakan GET dan POST. Kalau endpoint harus method-specific, cek sendiri atau pakai router yang mendukung method routing.
3. Menulis JSON tanpa set Content-Type
Client bisa salah mengira response adalah plain text. Set header sebelum encode.
4. Tidak menutup body atau membatasi ukurannya
Kalau request body tidak dibatasi, server kecil bisa lebih mudah kena payload besar atau abuse.
5. Tidak menangani error json.Decode
Kalau decode gagal dan handler tetap lanjut, data request bisa setengah terisi. Selalu fail fast.
6. Mengabaikan error response writer
Di banyak contoh sederhana, error Encode sering diabaikan. Di service kecil ini kadang lolos, tapi di production tetap perlu dipikirkan.
7. Panic yang tidak direcovery
Tanpa middleware recovery, satu panic bisa menutup request dan mencemari log. Minimal punya middleware recovery kalau server mulai dipakai serius.
8. Business logic terlalu banyak di handler
Handler sebaiknya tipis. Kalau logic terlalu tebal, pindahkan ke service layer supaya mudah dites dan diubah.
9. Field JSON tag tidak konsisten
json:"user_name" dan json:"username" terlihat sepele, tapi bisa bikin payload tidak cocok dengan kontrak API.
10. Tidak menetapkan timeout server
Server tanpa timeout lebih rentan terhadap koneksi lambat atau request yang menggantung.
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}Kapan net/http saja sudah cukup
Pakai net/http saja kalau:
- jumlah endpoint masih sedikit
- routing sederhana
- kebutuhan middleware tidak banyak
- kamu ingin dependency minimal
- service ini bagian dari alat internal atau API kecil
Tambahkan router/middleware library kalau kebutuhan berikut mulai muncul:
- path parameter kompleks
- nested route group
- middleware per group
- observability yang lebih serius
- struktur aplikasi mulai besar dan banyak tim terlibat
Referensi mental yang enak dipakai
Kalau bingung, pakai urutan keputusan ini:
- apakah ini cuma HTTP handler biasa?
- apakah path-nya cukup dengan
ServeMux? - apakah logic ini cross-cutting dan cocok jadi middleware?
- apakah payload perlu decode/encode JSON?
- apakah server butuh timeout, logging, dan recovery?
Kalau jawaban pertanyaan itu jelas, implementasi Go biasanya akan ikut jelas juga.
Ringkasan
net/httpmemberi fondasi server HTTP yang cukup kuat tanpa framework besar.- Handler adalah titik masuk request; middleware membungkus handler untuk logic lintas endpoint.
ServeMuxcukup untuk routing sederhana, tapi method check tetap perlu ditangani.- JSON request dibaca dengan
json.Decoder, JSON response ditulis denganjson.Encoder. - Server Go kecil sering bocor di area timeout, body size, method handling, dan penempatan business logic.
Kalau kamu butuh, pola dasar ini bisa jadi fondasi yang bersih sebelum naik ke router yang lebih lengkap.