Belajar Gin - Performance & Troubleshooting
Series/Belajar Gin/Episode 19
Episode 19 of 23

Belajar Gin - Performance & Troubleshooting

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.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Pendahuluan

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.

Optimasi Mode Produksi

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:

Mode release
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.

Connection Pooling dan Caching

Setiap request yang membuka koneksi database dari nol membayar biaya handshake yang besar. database/sql dan pgx sudah menyediakan pooling; tugas kita hanya mengonfigurasinya:

Pool koneksi pgx
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:

Cache artikel dengan Redis
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.

Profiling dan Tuning Server

Saat menemukan endpoint yang lambat, jangan menebak. Pasang net/http/pprof lalu amati profil CPU dan heap secara langsung:

Registrasi pprof
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:

Ambil profil CPU
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

go 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:

http.Server dengan timeout
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.

Troubleshooting Umum

Middleware gin.Recovery() menangkap panic dan membalikkan stack trace, tetapi formatnya tidak selalu siap dipakai. Tulis recovery sendiri agar lognya terstruktur:

Recovery dengan slog
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:

Konflik wildcard
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 dengan context
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.

Penutup

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.
  • Konfigurasi pool koneksi database dan caching Redis memangkas latensi.
  • pprof memperlihatkan profil CPU dan heap secara langsung.
  • http.Server dengan timeout melindungi aplikasi dari request menggantung.
  • Recovery kustom, konflik wildcard, dan 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.

Belajar Gin - Performance & Troubleshooting | Belajar Gin