Episode ini membedah optimasi dan troubleshooting aplikasi Gin: gin.SetMode ReleaseMode, mengurangi alokasi dan reuse struct, connection pooling serta caching Redis, profiling dengan pprof, tuning http.Server, dan menangani masalah umum seperti panic, routing conflict, deadlock context, dan memory leak goroutine.

Episode 18 menjadikan aplikasi kita bisa diamati; episode 19 ini membuatnya cepat dan mudah ditelusuri saat bermasalah. Kita akan membedah performance & troubleshooting Gin: mode release, pengurangan alokasi, connection pooling dan caching Redis, profiling dengan pprof, tuning http.Server, serta penanganan masalah umum seperti panic, routing conflict, deadlock context, dan memory leak goroutine.
Performa bukan hanya angka di benchmark. Aplikasi yang sehat adalah yang lambatnya bisa dipahami dan anehnya bisa ditelusuri, sehingga optimasi dan troubleshooting dibahas beriringan.
Kesalahan paling umum yang membuat aplikasi Gin terasa lambat di production adalah tetap berjalan dalam mode debug. Mode ini mencetak log rute, menulis warning, dan memproses hal yang tidak dibutuhkan saat melayani traffic nyata:
func main() {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}gin.SetMode(gin.ReleaseMode) menonaktifkan pesan debug dan warning pencocokan rute, lalu router memakai jalur yang lebih efisien. Nilai yang sama bisa diberikan lewat environment variable GIN_MODE=release. Pasang mode ini sebelum membuat engine.
Titik paling mahal lainnya di hot path adalah serialisasi JSON: c.JSON membuat buffer dan mengalokasi pada setiap panggilan. Untuk respons dengan struktur yang sudah diketahui, tulis langsung ke writer dengan json.NewEncoder(c.Writer).Encode(data) agar tidak menumpuk seluruh hasil di memori. Untuk objek yang dipakai berulang antar-request, gunakan sync.Pool agar alokasinya di-reuse, tetapi pastikan objek tidak lagi dipakai setelah dikembalikan ke pool.
Setiap request yang membuka koneksi database dari nol membayar biaya handshake yang besar. database/sql dan pgx sudah menyediakan pooling; tugas kita hanya mengonfigurasinya:
cfg, _ := pgxpool.ParseConfig("postgres://app:pass@db:5432/app")
cfg.MaxConns = 20
cfg.MaxConnIdleTime = 5 * time.Minute
cfg.MaxConnLifetime = 30 * time.Minute
pool, err := pgxpool.NewWithConfig(context.Background(), cfg)
if err != nil {
panic(err)
}pgxpool.NewWithConfig(...) membuka kumpulan koneksi yang dipakai bersama. MaxConns membatasi beban ke database, sedangkan MaxConnIdleTime dan MaxConnLifetime mencegah koneksi basi dan kebocoran di sisi database.
Untuk data yang jarang berubah namun sering dibaca, cache di Redis mengurangi beban database secara drastis:
func cachedArticle(c *gin.Context) {
key := "article:" + c.Param("id")
val, err := rdb.Get(c.Request.Context(), key).Result()
if err == nil {
c.Data(200, "application/json", []byte(val))
return
}
if !errors.Is(err, redis.Nil) {
slog.Error("redis get", "error", err)
}
payload, _ := json.Marshal(data)
rdb.Set(c.Request.Context(), key, payload, 5*time.Minute)
c.Data(200, "application/json", payload)
}rdb.Get(...).Result() membaca cache terlebih dahulu; redis.Nil berarti cache kosong, lalu data diambil dari database dan diisi kembali dengan TTL lima menit. Selalu batasi TTL agar cache tidak basi.
Saat menemukan endpoint yang lambat, jangan menebak. Pasang net/http/pprof lalu amati profil CPU dan heap secara langsung:
func registerPprof(r *gin.Engine) {
r.GET("/debug/pprof/*pprof", gin.WrapF(pprof.Index))
r.GET("/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline))
r.GET("/debug/pprof/profile", gin.WrapF(pprof.Profile))
r.GET("/debug/pprof/symbol", gin.WrapF(pprof.Symbol))
r.GET("/debug/pprof/trace", gin.WrapF(pprof.Trace))
}gin.WrapF(pprof.Index) membungkus handler pprof standar agar bisa dipasang di route Gin. Amati profil CPU selama 30 detik:
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30go tool pprof membuka antarmuka interaktif; ketik top untuk melihat fungsi paling berat dan web untuk grafik flame. Analisis heap dengan go tool pprof http://localhost:8080/debug/pprof/heap untuk menemukan alokasi yang tak terkendali. Di production, jangan pernah mengekspos pprof ke publik.
gin.Engine hanyalah handler; server HTTP sebenarnya adalah http.Server. Gunakan struct ini secara langsung agar timeout bisa diatur:
server := &http.Server{
Addr: ":8080",
Handler: r,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server stopped", "error", err)
}server.ListenAndServe() menjalankan server dengan timeout yang melindungi aplikasi dari request menggantung. ReadHeaderTimeout wajib diisi untuk mencegah serangan slowloris; IdleTimeout menjaga koneksi keep-alive tidak menumpuk. Kombinasikan dengan graceful shutdown dari episode 10.
Middleware gin.Recovery() menangkap panic dan membalikkan stack trace, tetapi formatnya tidak selalu siap dipakai. Tulis recovery sendiri agar lognya terstruktur:
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recovered",
"error", err,
"path", c.FullPath(),
)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "internal server error",
})
}
}()
c.Next()
}
}c.AbortWithStatusJSON(...) menghentikan eksekusi dan mengembalikan respons JSON yang seragam. Jangan bocorkan detail panic ke klien; simpan detailnya di log untuk insinyur.
Gin memakai radix tree, sehingga dua rute tidak boleh menaruh wildcard berbeda pada posisi yang sama. Kode di bawah memicu panic saat registrasi:
r.GET("/users/:id", userByID)
r.GET("/users/:name", userByName) // panic: wildcard conflict
r.GET("/users/:id", userByID)
r.GET("/users/by-name/:name", userByName)Wildcard :name bertabrakan dengan :id pada prefix yang sama; solusinya memakai nama parameter yang konsisten atau memisahkan jalurnya. panic: ':name' in new path '/users/:name' conflicts... muncul tepat saat go run, bukan saat runtime — perhatikan pesannya dan sesuaikan jalurnya. Menjalankan go run di pipeline CI menangkap konflik ini lebih awal.
Handler yang menunggu hasil dari goroutine lain sering menggantung ketika klien membatalkan request. Gunakan context request di dalam select:
select {
case result := <-respCh:
c.JSON(200, result)
case <-c.Request.Context().Done():
slog.Warn("client disconnected", "path", c.FullPath())
}c.Request.Context().Done() menutup kanal saat klien pergi, sehingga handler tidak menunggu selamanya. Ini mencegah dua masalah sekaligus: deadlock karena kanal tidak pernah mengirim, dan memory leak karena goroutine menunggu tanpa batas. Batasi waktu menunggu dengan timeout, misalnya time.After, supaya goroutine dijamin selesai.
Inti yang harus dibawa pulang:
gin.SetMode(gin.ReleaseMode) menonaktifkan debug dan mempercepat router.json.NewEncoder(c.Writer) dan sync.Pool mengurangi alokasi di hot path.http.Server dengan timeout melindungi aplikasi dari request menggantung.select dengan context menangani masalah umum.Di episode 20 selanjutnya kita akan membedah fitur stabil terbaru Gin — v1.11 dan v1.12, termasuk binding baru, OptionFunc dan engine.With, serta jalur upgrade yang mulus dari versi yang kalian pakai sekarang.