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
}

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/http sudah sangat lengkap — tidak selalu butuh framework; tambahkan dependency hanya jika benar-benar diperlukan.
  • Selalu konfigurasi timeout pada http.ServerReadTimeout, WriteTimeout, IdleTimeout wajib ada di production.
  • Graceful shutdown dengan srv.Shutdown(ctx) agar request aktif selesai sebelum server berhenti.
  • Go 1.22+: ServeMux sudah 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 gunakan io.LimitReader saat membaca body untuk mencegah memory exhaustion.
  • Tulis header sebelum WriteHeader — setelah WriteHeader dipanggil, header tidak bisa diubah lagi.
  • HTTP client production: buat &http.Client{Timeout: ...} sendiri — http.DefaultClient tidak 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:embed dan http.FS() — tidak perlu file eksternal saat deployment.

← Sebelumnya: Web Socket   Berikutnya: Unit Test →

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