Web Server #
Package net/http di Go adalah salah satu standard library terkuat di dunia — kamu bisa membangun web server production-grade tanpa satu pun dependency eksternal. Banyak tim Go memilih untuk tidak menggunakan framework sama sekali karena net/http sudah mencakup hampir semua kebutuhan: routing, middleware, static files, HTTPS, HTTP/2, timeout, dan graceful shutdown. Artikel ini membahas semua yang perlu kamu tahu untuk membangun web server yang benar di Go.
http.Handler — Fondasi Segalanya
#
Seluruh net/http dibangun di atas satu interface:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Apapun yang mengimplementasikan ServeHTTP adalah handler yang valid. http.HandlerFunc adalah tipe adapter yang mengubah fungsi biasa menjadi http.Handler:
// Fungsi biasa
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Halo, Dunia!")
}
// Konversi ke Handler
var handler http.Handler = http.HandlerFunc(hello)
// Shortcut via HandleFunc — paling sering dipakai
http.HandleFunc("/hello", hello)
http.ServeMux — Router Bawaan
#
http.ServeMux adalah multiplexer request bawaan Go. Sejak Go 1.22, ServeMux mendukung method routing dan path parameters tanpa library eksternal:
mux := http.NewServeMux()
// Go 1.22+: method + path
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser) // path parameter
mux.HandleFunc("PUT /users/{id}", updateUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
// Cara lama (semua method, semua versi Go)
mux.HandleFunc("/api/", apiHandler) // trailing slash = prefix match
mux.HandleFunc("/health", healthCheck) // exact match
// Ambil path parameter (Go 1.22+)
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // ambil {id} dari path
fmt.Fprintf(w, "User ID: %s", id)
}
http.Server — Konfigurasi yang Benar
#
Jangan pakai http.ListenAndServe langsung di production — ia tidak mengatur timeout sehingga rentan terhadap Slowloris attack:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
// Timeout wajib untuk production
ReadTimeout: 5 * time.Second, // batas waktu baca seluruh request
ReadHeaderTimeout: 2 * time.Second, // batas waktu baca header saja
WriteTimeout: 10 * time.Second, // batas waktu tulis response
IdleTimeout: 120 * time.Second, // batas waktu koneksi idle (keep-alive)
MaxHeaderBytes: 1 << 20, // 1MB max header size
}
log.Fatal(srv.ListenAndServe())
Graceful Shutdown #
Server yang bisa berhenti dengan bersih — menyelesaikan request yang sedang berjalan sebelum exit:
func main() {
mux := http.NewServeMux()
// ... daftarkan routes
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Jalankan server di goroutine terpisah
go func() {
log.Println("Server berjalan di :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal("ListenAndServe:", err)
}
}()
// Tunggu sinyal OS
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Menerima sinyal shutdown...")
// Beri waktu 30 detik untuk menyelesaikan request aktif
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Shutdown paksa:", err)
}
log.Println("Server berhenti dengan bersih")
}
Membaca Request #
func handler(w http.ResponseWriter, r *http.Request) {
// Method dan URL
fmt.Println(r.Method) // GET, POST, dll
fmt.Println(r.URL.Path) // /users/42
fmt.Println(r.URL.String()) // /users/42?sort=name
// Query parameters
name := r.URL.Query().Get("name") // ?name=budi
tags := r.URL.Query()["tags"] // ?tags=a&tags=b → []string
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
// Headers
contentType := r.Header.Get("Content-Type")
token := r.Header.Get("Authorization")
// Path value (Go 1.22+)
id := r.PathValue("id")
// Body — hanya POST/PUT/PATCH
defer r.Body.Close()
// Baca sebagai bytes
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // max 1MB
if err != nil {
http.Error(w, "gagal baca body", http.StatusBadRequest)
return
}
// Decode JSON body
var payload struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "JSON tidak valid: "+err.Error(), http.StatusBadRequest)
return
}
// Form data
r.ParseForm()
username := r.FormValue("username")
_ = username
// Multipart form (file upload)
r.ParseMultipartForm(10 << 20) // 10MB
file, header, err := r.FormFile("avatar")
if err == nil {
defer file.Close()
fmt.Println("Upload:", header.Filename, header.Size)
}
// Cookie
cookie, err := r.Cookie("session_id")
if err == nil {
fmt.Println("Session:", cookie.Value)
}
_ = name; _ = tags; _ = page; _ = contentType; _ = token; _ = id; _ = body
}
Menulis Response #
func respond(w http.ResponseWriter, r *http.Request) {
// Set header sebelum WriteHeader atau Write
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Request-ID", "abc123")
// Set status code (default 200 jika tidak dipanggil)
w.WriteHeader(http.StatusCreated) // 201
// Tulis body
json.NewEncoder(w).Encode(map[string]any{
"id": 42,
"message": "berhasil dibuat",
})
}
// Helper untuk JSON response yang konsisten
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, APIResponse{Success: false, Error: msg})
}
// Redirect
http.Redirect(w, r, "/login", http.StatusFound) // 302
// Set cookie
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: "abc123",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 86400, // 1 hari
})
Middleware #
Middleware adalah fungsi yang membungkus handler untuk menambahkan fungsionalitas. Pola standarnya:
type Middleware func(http.Handler) http.Handler
// Logging middleware
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap ResponseWriter untuk menangkap status code
rw := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(rw, r)
log.Printf("%s %s %d %v", r.Method, r.URL.Path, rw.status, time.Since(start))
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(status int) {
rw.status = status
rw.ResponseWriter.WriteHeader(status)
}
// Recovery middleware — tangkap panic agar server tidak crash
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// CORS middleware
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// Auth middleware
func requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !strings.HasPrefix(token, "Bearer ") {
writeError(w, http.StatusUnauthorized, "token diperlukan")
return
}
// validasi token...
next.ServeHTTP(w, r)
})
}
// Chaining middleware — terapkan dari kanan ke kiri
func chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
// Penggunaan
handler := chain(mux, logging, recovery, cors)
Context — Meneruskan Data Antar Middleware #
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
// Middleware yang menyimpan data ke context
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := generateRequestID()
ctx := context.WithValue(r.Context(), requestIDKey, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Handler yang mengambil data dari context
func getHandler(w http.ResponseWriter, r *http.Request) {
reqID := r.Context().Value(requestIDKey).(string)
log.Printf("[%s] Handling request", reqID)
}
Static Files #
// Serve direktori lokal
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
// Serve embedded files (Go 1.16+)
//go:embed static/*
var staticFiles embed.FS
subFS, _ := fs.Sub(staticFiles, "static")
mux.Handle("/static/", http.StripPrefix("/static/",
http.FileServer(http.FS(subFS))))
// Serve satu file
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./static/favicon.ico")
})
HTTP Client #
net/http juga menyediakan client HTTP yang powerful:
// Jangan pakai http.DefaultClient di production — tidak ada timeout!
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
DisableCompression: false,
},
}
// GET
resp, err := client.Get("https://api.example.com/users")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var users []User
json.NewDecoder(resp.Body).Decode(&users)
// POST JSON
payload, _ := json.Marshal(map[string]string{"name": "Budi"})
resp2, err := client.Post("https://api.example.com/users",
"application/json", bytes.NewReader(payload))
// Request kustom dengan header
req, _ := http.NewRequestWithContext(ctx, "DELETE",
"https://api.example.com/users/42", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp3, err := client.Do(req)
defer resp3.Body.Close()
Contoh Program Lengkap — REST API #
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"time"
)
// ── Model ─────────────────────────────────────────────────────
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
Category string `json:"category"`
}
// ── In-Memory Store ───────────────────────────────────────────
type Store struct {
mu sync.RWMutex
products map[int]Product
nextID int
}
func NewStore() *Store {
s := &Store{products: make(map[int]Product)}
// Seed data
for _, p := range []Product{
{Name: "Laptop Pro", Price: 15_000_000, Stock: 10, Category: "elektronik"},
{Name: "Mouse Wireless", Price: 350_000, Stock: 50, Category: "elektronik"},
{Name: "Buku Go", Price: 180_000, Stock: 30, Category: "buku"},
} {
s.nextID++
p.ID = s.nextID
s.products[p.ID] = p
}
return s
}
func (s *Store) List() []Product {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]Product, 0, len(s.products))
for _, p := range s.products {
list = append(list, p)
}
return list
}
func (s *Store) Get(id int) (Product, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.products[id]
return p, ok
}
func (s *Store) Create(p Product) Product {
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
p.ID = s.nextID
s.products[p.ID] = p
return p
}
func (s *Store) Update(id int, p Product) (Product, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.products[id]; !ok {
return Product{}, false
}
p.ID = id
s.products[id] = p
return p, true
}
func (s *Store) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.products[id]; !ok {
return false
}
delete(s.products, id)
return true
}
// ── Handler ───────────────────────────────────────────────────
type Handler struct{ store *Store }
func (h *Handler) respond(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]any{"success": status < 400, "data": data})
}
func (h *Handler) respondError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]any{"success": false, "error": msg})
}
func (h *Handler) list(w http.ResponseWriter, r *http.Request) {
products := h.store.List()
// Filter by category
if cat := r.URL.Query().Get("category"); cat != "" {
filtered := products[:0]
for _, p := range products {
if p.Category == cat {
filtered = append(filtered, p)
}
}
products = filtered
}
h.respond(w, http.StatusOK, products)
}
func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
h.respondError(w, http.StatusBadRequest, "ID tidak valid")
return
}
p, ok := h.store.Get(id)
if !ok {
h.respondError(w, http.StatusNotFound, "produk tidak ditemukan")
return
}
h.respond(w, http.StatusOK, p)
}
func (h *Handler) create(w http.ResponseWriter, r *http.Request) {
var p Product
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
h.respondError(w, http.StatusBadRequest, "JSON tidak valid")
return
}
if p.Name == "" {
h.respondError(w, http.StatusBadRequest, "name wajib diisi")
return
}
created := h.store.Create(p)
h.respond(w, http.StatusCreated, created)
}
func (h *Handler) update(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
h.respondError(w, http.StatusBadRequest, "ID tidak valid")
return
}
var p Product
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
h.respondError(w, http.StatusBadRequest, "JSON tidak valid")
return
}
updated, ok := h.store.Update(id, p)
if !ok {
h.respondError(w, http.StatusNotFound, "produk tidak ditemukan")
return
}
h.respond(w, http.StatusOK, updated)
}
func (h *Handler) delete(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
h.respondError(w, http.StatusBadRequest, "ID tidak valid")
return
}
if !h.store.Delete(id) {
h.respondError(w, http.StatusNotFound, "produk tidak ditemukan")
return
}
h.respond(w, http.StatusOK, map[string]string{"message": "berhasil dihapus"})
}
// ── Middleware ────────────────────────────────────────────────
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 %v", r.Method, r.URL.Path, time.Since(start))
})
}
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// ── Main ──────────────────────────────────────────────────────
func main() {
store := NewStore()
h := &Handler{store: store}
mux := http.NewServeMux()
// Routes (Go 1.22+)
mux.HandleFunc("GET /api/products", h.list)
mux.HandleFunc("POST /api/products", h.create)
mux.HandleFunc("GET /api/products/{id}", h.get)
mux.HandleFunc("PUT /api/products/{id}", h.update)
mux.HandleFunc("DELETE /api/products/{id}", h.delete)
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// Terapkan middleware
handler := logging(cors(mux))
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
log.Println("REST API berjalan di :8080")
log.Println("Coba: curl http://localhost:8080/api/products")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
log.Println("Server berhenti")
}
Ringkasan #
net/httpsudah sangat lengkap — tidak selalu butuh framework; tambahkan dependency hanya jika benar-benar diperlukan.- Selalu konfigurasi timeout pada
http.Server—ReadTimeout,WriteTimeout,IdleTimeoutwajib ada di production.- Graceful shutdown dengan
srv.Shutdown(ctx)agar request aktif selesai sebelum server berhenti.- Go 1.22+:
ServeMuxsudah mendukung method routing (GET /path) dan path parameters ({id}) tanpa library eksternal.- Middleware diimplementasikan sebagai
func(http.Handler) http.Handler— composable dan bisa di-chain.- Selalu
defer r.Body.Close()dan gunakanio.LimitReadersaat membaca body untuk mencegah memory exhaustion.- Tulis header sebelum
WriteHeader— setelahWriteHeaderdipanggil, header tidak bisa diubah lagi.- HTTP client production: buat
&http.Client{Timeout: ...}sendiri —http.DefaultClienttidak punya timeout.- Context untuk meneruskan data (request ID, user) antar middleware dan handler tanpa parameter tambahan.
- Static files bisa di-embed ke binary dengan
//go:embeddanhttp.FS()— tidak perlu file eksternal saat deployment.