Episode ini memoles performa: pola rute yang efisien, tuning http.Server, connection pooling, dan caching dengan Redis. Kalian juga belajar memecahkan masalah umum seperti route pattern conflict, panic handler, deadlock context, kebocoran goroutine, serta strategi upgrade antar versi chi.

Aplikasi yang sudah benar belum tentu cepat. Episode 19 menutup sisi teknis: cara memompa performa tanpa mengorbankan kebenaran, lalu cara memecahkan masalah klasik yang menghantui setiap project Go production.
Bagian pertama membahas optimasi: pola rute, tuning server, pooling, dan caching. Bagian kedua membahas troubleshooting: gejala, akar masalah, dan solusi untuk lima masalah paling umum di ekosistem chi.
radix tree chi bekerja optimal dengan pola yang teratur:
r.Get("/users", listUsers)
r.Get("/users/{id:[0-9]+}", getUser)
r.Post("/users", createUser){id:[0-9]+} lebih efisien daripada param bebas untuk identifier numerik — regex lebih spesifik, lebih sedikit kandidat yang dicoba. Hindari wildcard {path:*} di rute yang sering dipanggil kecuali memang diperlukan.
Dua rute yang bisa cocok dengan request yang sama memperlambat matching dan membingungkan. Buat pola se-spesifik mungkin dan gunakan subrouter untuk mengelompokkan path yang berbagi prefix — radix tree akan berbagi node secara otomatis.
Konfigurasi server memberi perlindungan beban:
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20,
}ReadTimeout membatasi waktu membaca request; WriteTimeout membatasi penulisan respons. MaxHeaderBytes mencegah header raksasa. Nilai ini menurunkan peluang serangan lambat dan koneksi menggantung.
Tanpa timeout, koneksi yang lambat bisa menahan goroutine selama-lamanya — setiap koneksi memakan goroutine, dan goroutine yang menggantung menguras memory. Nilai di atas adalah titik awal yang wajar untuk API JSON.
Pool yang sudah dipakai sejak episode 9 perlu di-set untuk beban nyata:
pool.Config().MaxConns = 20
pool.Config().MinConns = 5
pool.Config().MaxConnLifetime = time.Hour
pool.Config().MaxConnIdleTime = 5 * time.MinuteMaxConns membatasi koneksi paralel maksimal ke database; MinConns menjaga koneksi siap pakai. MaxConnLifetime memaksa rotasi koneksi agar tidak memakai koneksi basi. Ukuran yang pas bergantung pada spesifikasi database.
Respons yang sering diminta jangan dihitung ulang:
go get github.com/redis/go-redis/v9client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
func cacheOrFetch(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
val, err := client.Get(ctx, "popular").Result()
if err == nil {
writeJSON(w, http.StatusOK, val)
return
}
data := fetchSlowData(ctx)
client.Set(ctx, "popular", data, time.Minute)
writeJSON(w, http.StatusOK, data)
}client.Get(ctx, "popular") membaca cache lebih dulu; bila kosong, data dihitung lalu disimpan dengan client.Set(ctx, "popular", data, time.Minute). TTL satu menit memastikan cache tidak basi selamanya.
Jangan cache data per-user yang sensitif di key global, dan jangan cache data yang berubah cepat tanpa invalidasi. Cache terbaik adalah untuk data yang jarang berubah dan mahal dihitung.
Gejala: rute didaftarkan tapi tidak pernah cocok, atau panic saat startup. Solusinya: radix tree chi melarang dua pola ambigu pada posisi yang sama. Periksa urutan — literal harus menang atas param, dan jangan daftarkan dua wildcard di segmen yang sama.
Gejala: server mati mendadak tanpa log. Solusi: pasang middleware.Recoverer paling atas r.Use:
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer)middleware.Recoverer mencetak stack trace ke log — bahan utama untuk menemukan baris yang panic.
Gejala: handler menggantung, timeout tak pernah jalan. Penyebab: context tidak diteruskan, atau channel menunggu tanpa select dengan ctx.Done(). Solusi: selalu seleksi dengan <-ctx.Done() (episode 12) dan teruskan req.Context() ke setiap operasi.
Gejala: memory naik terus meski beban stabil. Penyebab: goroutine tanpa jalan keluar, misal go func() yang menulis ke unbuffered channel tanpa pembaca. Solusi:
go func() {
select {
case ch <- result:
case <-ctx.Done():
}
}()select { case ch <- result: case <-ctx.Done(): } memberi goroutine jalan keluar saat request dibatalkan. Pantau dengan pprof go tool pprof untuk menemukan goroutine yang menggantung.
Gejala: kode lama rusak setelah go get versi baru. Solusinya: baca release notes, jalankan test dulu, dan perbarui bertahap:
go get github.com/go-chi/chi/v5@latest
go mod tidy
go test ./...go get github.com/go-chi/chi/v5@latest lalu go test ./... memastikan upgrade tidak merusak perilaku. Detail fitur per versi kita bahas di episode 20.
Inti yang harus dibawa pulang:
ReadTimeout dan WriteTimeout melindungi dari request lambat.Di episode 20 selanjutnya kita melihat yang terbaru: fitur stabil terbaru chi v5.2 dan v5.3 — dukungan empat versi Go terakhir, middleware.Discard dan perbaikan RedirectSlashes di v5.2, serta inline middleware di subrouter, dukungan XML di default compressible types, penggantian io/ioutil, dan CI Go 1.26 di v5.3.