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

Anatomi satu request — dari klien sampai baris database

Materi ini menyatukan semua yang sudah dibahas jadi satu jalur. Setelahnya, setiap keputusan di sisa roadmap — timeout, pool, cache, deploy — punya tempat yang jelas di peta.

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

Intisari

  • Permintaan melewati tujuh batas; tiap batas punya timeout dan tiap timeout harus mengecil ke dalam.
  • Sebagian besar permintaan pada situs bertrafik baca tinggi seharusnya berhenti di CDN dan tidak pernah menyentuh Go.
  • Di dalam proses, satu permintaan = satu goroutine + satu context.Context + satu koneksi database yang dipinjam sebentar.
  • Koneksi database dipinjam dari pool per-kueri, bukan per-permintaan. Menahannya lebih lama dari perlu adalah penyebab kemacetan nomor satu.
  • Kalau ctx tidak diteruskan sampai ke QueryContext, rantai pembatalannya putus dan seluruh anggaran waktu jadi hiasan.

Peta lengkapnya

  Browser
     │  1. DNS → Route 53
     ▼
  CloudFront (CDN)                     ← cache hit: SELESAI di sini
     │  2. cache miss / konten personal
     ▼
  ALB (terminasi TLS, health check)
     │  3. pilih target sehat
     ▼
  ECS Fargate task ── proses Go
     │
     ├─ net/http.Server menerima koneksi
     │     └─ goroutine BARU per permintaan
     │
     ├─ Middleware  (recover → request ID → log → timeout → CORS → rate limit → auth)
     │     └─ r.Context() kini punya: tenggat, request ID, identitas pengguna
     │
     ├─ ServeMux mencocokkan "GET /produk/{id}"
     │
     ├─ Handler: parse & validasi input  ──► 400 kalau gagal
     │
     ├─ Service (domain): aturan bisnis   ──► 403 / 409 kalau melanggar
     │
     ├─ Repository: pinjam koneksi dari pool
     │     ├─ pool penuh? TUNGGU (sampai ctx habis)
     │     ▼
     │  PostgreSQL (RDS) ── kueri berjalan, index dipakai, baris kembali
     │     ▲
     │     └─ koneksi DIKEMBALIKAN ke pool  ← secepat mungkin
     │
     ├─ Service: susun hasil jadi objek domain
     ├─ Handler: petakan ke DTO respons, tulis header + status + body
     │
     └─ Middleware selesai dari dalam ke luar: log dicatat, panic dipulihkan
     ▼
  ALB ► CloudFront (menyimpan sesuai Cache-Control) ► Browser

Batas 1–3: sebelum kodemu jalan

BatasYang terjadiYang kamu kendalikan
CDNCache dicek dengan kunci: host + path + header/query terpilihHeader Cache-Control yang kodemu kirim
Load balancerMemilih target yang lolos health checkEndpoint /sehat, idle timeout, algoritma
ContainerMenerima koneksi TCPReadHeaderTimeout, jumlah task

Pengungkit terbesar ada di baris pertama tabel ini. Untuk halaman yang sama bagi semua pembaca, satu header Cache-Control: public, s-maxage=60, stale-while-revalidate=600 memindahkan sebagian besar lalu lintas ke CloudFront — permintaan yang tidak pernah menyentuh Go, tidak pernah menyentuh database, dan tidak menambah satu pun kelas bug. Dibahas di Fase 9.

Batas 4: masuk ke proses Go

// Yang sudah terjadi sebelum baris pertama handler-mu:
//  1. Koneksi diterima, header dibaca (dibatasi ReadHeaderTimeout)
//  2. Goroutine baru dibuat khusus untuk permintaan ini
//  3. *http.Request dibangun; r.Context() dibatalkan otomatis kalau klien putus
//  4. Rantai middleware dijalankan dari luar ke dalam

func (h *Handler) AmbilProduk(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()   // ← bawa ini ke MANA-MANA. Ini benang merahnya.

	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
	if err != nil || id <= 0 {
		h.tulisError(w, r, &ErrValidasi{Field: "id", Pesan: "harus angka positif"})
		return
	}

	p, err := h.layanan.Ambil(ctx, id)     // ctx diteruskan
	if err != nil {
		h.tulisError(w, r, err)             // errors.Is memetakan ke status
		return
	}

	h.tulisJSON(w, http.StatusOK, keProdukResp(p))
}

Batas 5: handler ke domain

// internal/produk/layanan.go — tidak tahu apa itu HTTP
func (s *Layanan) Ambil(ctx context.Context, id int64) (Produk, error) {
	// Cache dulu: hit di sini menghemat perjalanan ke database.
	if p, ok := s.cache.Ambil(ctx, id); ok {
		return p, nil
	}

	// singleflight: seribu permintaan bersamaan untuk id yang sama
	// menghasilkan SATU kueri (Fase 2).
	v, err, _ := s.grup.Do(strconv.FormatInt(id, 10), func() (any, error) {
		return s.penyimpan.Ambil(ctx, id)
	})
	if err != nil {
		return Produk{}, err
	}

	p := v.(Produk)
	s.cache.Simpan(ctx, p, 60*time.Second)
	return p, nil
}

Batas 6: domain ke database

// internal/produk/postgres.go
func (r *Postgres) Ambil(ctx context.Context, id int64) (Produk, error) {
	// Tenggat khusus kueri — lebih ketat dari tenggat permintaan.
	ctx, batal := context.WithTimeout(ctx, 2*time.Second)
	defer batal()

	var p Produk
	err := r.db.QueryRowContext(ctx, `
		SELECT id, nama, harga, dibuat
		FROM produk
		WHERE id = $1 AND dihapus_pada IS NULL`, id,
	).Scan(&p.ID, &p.Nama, &p.Harga, &p.Dibuat)

	switch {
	case errors.Is(err, sql.ErrNoRows):
		return Produk{}, fmt.Errorf("produk %d: %w", id, ErrTidakDitemukan)
	case err != nil:
		return Produk{}, fmt.Errorf("ambil produk %d: %w", id, err)
	}
	return p, nil
}

Apa yang sebenarnya terjadi di baris QueryRowContext

  1. database/sql meminta koneksi dari pool. Kalau semua sedang dipakai dan MaxOpenConns tercapai, ia menunggu — dan penungguan itu dibatasi ctx.
  2. Kueri dikirim sebagai prepared statement dengan parameter terpisah. Inilah yang membuat SQL injection tidak mungkin (Fase 4 dan 6).
  3. PostgreSQL merencanakan kueri, memakai index kalau ada, dan mengirim barisnya.
  4. Scan menyalin nilai ke variabel Go, dan koneksi dikembalikan ke pool.
  5. Kalau ctx kedaluwarsa di tengah, driver mengirim pembatalan ke server dan kueri dihentikan di sisi database juga.

Poin 4 adalah yang paling sering dirusak. Pada QueryContext (banyak baris), koneksi tidak dikembalikan sampai rows.Close() dipanggil. Sebuah rows yang lupa ditutup menahan satu koneksi selamanya — dan setelah MaxOpenConns permintaan seperti itu, seluruh aplikasi berhenti melayani sambil menunggu pool yang tidak akan pernah kosong. Selalu defer rows.Close().

Perjalanan pulang

Repository  → objek domain
Service     → aturan diterapkan, cache diisi
Handler     → domain dipetakan ke DTO, header + status + body ditulis
Middleware  → selesai dari dalam ke luar:
                CORS menambah header
                Timeout membatalkan context (defer batal())
                Log mencatat status + durasi
                Recover memastikan tidak ada panic yang lolos
Server      → koneksi di-keep-alive untuk permintaan berikutnya
ALB         → meneruskan
CloudFront  → menyimpan kalau Cache-Control mengizinkan

Anggaran waktu di sepanjang jalur

BatasSetelanNilai contoh
CloudFrontOrigin response timeout30 dtk
ALBIdle timeout60 dtk
Server GoReadTimeout / WriteTimeout15 dtk
Middlewarecontext.WithTimeout10 dtk
Service → API luartimeout klien HTTP3 dtk + retry
Repository → databasecontext.WithTimeout2 dtk

Aturannya: setiap lapisan lebih ketat daripada yang membungkusnya. Kalau timeout database lebih longgar daripada timeout permintaan, klien sudah menyerah sementara kueri tetap berjalan dan tetap memegang koneksi. Di bawah beban, itu berubah jadi lingkaran setan: makin banyak yang menyerah, makin penuh pool, makin lambat semuanya.

Di mana waktunya habis — dan di mana memperbaikinya

GejalaKemungkinan besarDibahas di
p95 tinggi, p50 normalAntrean pool database, atau GCFase 4, 9
Semua endpoint melambat bersamaanPool habis, atau satu kueri lambat memblokirFase 4
Lambat hanya pada daftarIndex hilang, atau N+1 kueriFase 4
Cepat lokal, lambat di AWSLatensi jaringan × jumlah kueri per permintaanFase 4, 10
CPU tinggi tanpa trafik tinggiSerialisasi JSON, kompresi, atau alokasi berlebihanFase 9
Rasio hit CDN rendahKunci cache mengandung cookie atau query pelacakanFase 9, 10

Latihan: gambar ulang peta di atas untuk satu endpoint di aplikasimu sendiri, lalu tulis angka timeout aktual di setiap batas. Tandai batas mana yang belum punya timeout sama sekali — itu daftar pekerjaanmu untuk sisa roadmap ini.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.