← Semua pembelajaran / Go Nol → Enterprise
Fase 3 · HTTP — Request sampai Response

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.

Sumber asli pkg.go.dev Resmi Rangkuman ~8 menit baca

Intisari

  • http.ListenAndServe cukup 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.
  • Handler hanyalah 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:

AkibatArtinya untuk kodemu
Handler berjalan paralelVariabel level paket yang ditulis handler adalah data race (Fase 2)
Tidak perlu go sendiriParalelisme antar permintaan sudah gratis
Struct handler dipakai bersamaIsinya hanya boleh dependensi yang aman dipakai bersamaan — *sql.DB, *slog.Logger, klien HTTP
Panic mematikan satu permintaanServer memulihkannya, tapi pasang middleware sendiri (Fase 1)
Goroutine bocor = kebocoran per permintaanYang 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

KebutuhanDi net/http
Routing dengan metode & parameterServeMux sejak Go 1.22
MiddlewareFungsi yang membungkus Handler
Berkas statishttp.FileServer + embed.FS
Template HTMLhtml/template, dengan escaping otomatis
Klien HTTP dengan poolhttp.Client
Tes tanpa servernet/http/httptest
Graceful shutdownsrv.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.