Membuka koneksi ke PostgreSQL butuh handshake TCP, autentikasi, dan beberapa round-trip — mahal bila dilakukan untuk setiap request. Karena itu sql.DB bukan satu koneksi, melainkan connection pool: sekadar objek manajer yang membuat, menyimpan, dan menutup koneksi di belakang layar. Aplikasi berumur panjang — web server, worker, daemon — menanggung satu pertanyaan yang tidak dihadapi skrip sekali jalan: bagaimana menjaga pool tetap sehat selama berhari-hari tanpa restart? Chapter penutup ini merangkum jawabannya: buka sekali di awal, bagikan ke seluruh komponen, pantau lewat Stats(), dan tutup rapi saat shutdown.
Pool Berumur Panjang #
Pola penampungnya biasa bernama App atau Server — struct yang membawa seluruh dependensi hidup, pool database termasuk salah satunya:
type App struct {
db *sql.DB
}
func main() {
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer db.Close() // shutdown: ditutup sekali saat program berakhir
db.SetMaxOpenConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
app := &App{db: db}
app.Run() // melayani HTTP, job latar, dsb.
}Fragmen di atas membutuhkan PostgreSQL aktif untuk dijalankan — aplikasi lengkapnya ada pada proyek REST API PostgreSQL
. Perhatikan dua hal: sql.Open tidak langsung membuat koneksi (koneksi dibuat malas saat query pertama — validasi koneksi lebih awal dilakukan lewat db.Ping()), dan defer db.Close() ditempatkan di main sehingga penutupan terjadi tepat sekali.
Demo Siklus Hidup Tanpa Database #
Siklus open-ambil-lepas-close bisa diamati tanpa PostgreSQL lewat pool tiruan. Berkas connection-demo.go membangun Pool in-memory dengan method yang meniru sql.DB: CobaAmbil mengambil koneksi, Kembalikan melepaskannya, Stats melaporkan kondisi, dan Close menutup pool:
package main
import "fmt"
// Stats meniru sql.DBStats: angka untuk memantau kesehatan pool.
type Stats struct {
MaxOpenConnections int
OpenConnections int
InUse int
WaitCount int64
}
// Pool adalah tiruan *sql.DB: dibuka sekali, dipakai bersama
// seluruh handler, dan ditutup sekali saat aplikasi berhenti.
type Pool struct {
maxOpen int
terpakai int
menunggu int64
ditutup bool
}
// OpenPool membuka pool dengan batas koneksi maksimum.
// sql.Open pada database nyata tidak langsung terhubung;
// koneksi dibuat malas saat query pertama.
func OpenPool(maxOpen int) *Pool {
return &Pool{maxOpen: maxOpen}
}
// CobaAmbil mengambil satu koneksi. Bila pool penuh, pemanggil
// masuk antrean (WaitCount naik) dan permintaan ini ditolak.
func (p *Pool) CobaAmbil() bool {
if p.ditutup {
return false
}
if p.terpakai >= p.maxOpen {
p.menunggu++ // pemanggil menunggu koneksi dikembalikan
return false
}
p.terpakai++
return true
}
// Kembalikan melepas koneksi agar bisa dipakai pemanggil lain.
func (p *Pool) Kembalikan() {
if p.terpakai > 0 {
p.terpakai--
}
}
// Stats melaporkan kondisi pool saat ini.
func (p *Pool) Stats() Stats {
return Stats{
MaxOpenConnections: p.maxOpen,
OpenConnections: p.terpakai,
InUse: p.terpakai,
WaitCount: p.menunggu,
}
}
// Close menutup pool: semua koneksi dilepas dan pool menolak
// permintaan baru. Pada database nyata, Close menutup semua
// koneksi idle sekaligus koneksi yang sedang dipakai.
func (p *Pool) Close() {
p.ditutup = true
}
func main() {
// Inisialisasi sekali di awal, bukan sekali per request.
pool := OpenPool(2)
defer pool.Close() // shutdown: tutup saat program selesai
fmt.Println("ambil pertama :", pool.CobaAmbil())
fmt.Println("ambil kedua :", pool.CobaAmbil())
s := pool.Stats()
fmt.Printf("stats : inUse=%d/%d waitCount=%d\n", s.InUse, s.MaxOpenConnections, s.WaitCount)
// Ambil ketiga: pool penuh, pemanggil menunggu.
fmt.Println("ambil ketiga :", pool.CobaAmbil())
pool.Kembalikan()
s = pool.Stats()
fmt.Printf("stats : inUse=%d/%d waitCount=%d\n", s.InUse, s.MaxOpenConnections, s.WaitCount)
fmt.Println("ambil ulang :", pool.CobaAmbil())
pool.Kembalikan()
pool.Kembalikan()
pool.Close()
fmt.Println("setelah close :", pool.CobaAmbil())
}Jalankan berkasnya dari direktori chapter:
go run connection-demo.goOutput yang Dihasilkan:
ambil pertama : true
ambil kedua : true
stats : inUse=2/2 waitCount=0
ambil ketiga : false
stats : inUse=1/2 waitCount=1
ambil ulang : true
setelah close : falseMari kita telaah bagian-bagian penting berkas tersebut:
- Baris 25-27 (
OpenPool): pool dibuat sekali dengan batas dua koneksi — penggantisql.Openbeserta konfigurasinya. - Baris 31-41 (
CobaAmbil): pengambilan koneksi. Bila pool penuh,menunggunaik (tiruanWaitCount) dan permintaan ditolak;sql.DBnyata membiarkan pemanggil menunggu sampai koneksi tersedia atau ctx dibatalkan. - Baris 43-48 (
Kembalikan): melepas koneksi — setararows.Close()atau selesainya transaksi yang mengembalikan koneksi ke pool. - Baris 50-58 (
Stats): laporan kondisi pool pada satu titik waktu, penggantidb.Stats(). - Baris 67-70 (main): pola inti chapter ini — pool dibuka sekali di awal
main, dibagikan lewat variabel, dandefer pool.Close()menutupnya saat program selesai. - Baris 79-85 (pool penuh): percobaan ketiga ditolak dan
waitCountnaik menjadi 1; begitu satu koneksi dikembalikan, permintaan berikutnya berhasil lagi — perilaku antrean pool nyata.
Konfigurasi Pool #
Empat pengaturan menentukan perilaku sql.DB di bawah beban:
| Method | Peran | Nilai awal yang wajar |
|---|---|---|
SetMaxOpenConns(n) | Batas total koneksi | 10-25, sesuai kapasitas database |
SetMaxIdleConns(n) | Koneksi idle yang disimpan | Sama dengan MaxOpen untuk beban stabil |
SetConnMaxLifetime(d) | Umur maksimum satu koneksi | 30 menit, agar DNS/load-balancer tidak basi |
SetConnMaxIdleTime(d) | Umur koneksi yang menganggur | 5-10 menit, melepas koneksi sepi |
Tanpa SetMaxOpenConns, jumlah koneksi tidak dibatasi — database bisa kewalahan saat lonjakan trafik. Tanpa SetConnMaxLifetime, koneksi hidup selamanya; infrastruktur di tengah jalan (proxy, load balancer, failover database) diam-diam memutus koneksi yang dianggap basi, dan aplikasi menerima error kejutan. Nilai default SetMaxIdleConns (2) juga terlalu kecil untuk banyak aplikasi — koneksi terus dibuka dan ditutup padahal bebannya stabil.
Memantau dengan Stats() #
db.Stats() mengembalikan sql.DBStats — potret pool pada satu saat. Dua angka paling penting: OpenConnections (total koneksi terbuka) dan WaitCount (akumulasi pemanggil yang pernah menunggu koneksi). WaitCount yang terus naik berarti pool terlalu kecil atau ada query yang terlalu lambat menahan koneksi:
s := db.Stats()
slog.Info("statistik pool",
"open", s.OpenConnections,
"inUse", s.InUse,
"idle", s.Idle,
"wait", s.WaitCount,
)Angka-angka ini layak diekspor ke sistem monitoring bersama metrik aplikasi lain — pembahasannya ada pada section Logging dan Observability .
Koneksi Basah Setelah Restart Database #
Database yang direstart atau failover membuat seluruh koneksi pool putus diam-diam. database/sql sudah menangani sebagian besar kasus: koneksi rusak dikenali, dibuang, dan query otomatis dicoba ulang pada koneksi baru. Dua lapisan pelindung tambahan tetap berguna. Pertama, health check periodik lewat db.Ping() — misalnya dari endpoint kesehatan aplikasi:
func healthz(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
http.Error(w, "database tidak siap", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}
}Kedua, graceful shutdown: hentikan penerimaan request baru (misalnya http.Server.Shutdown), tunggu request berjalan selesai, lalu panggil db.Close() — urutan yang dibahas tuntas pada chapter Graceful Shutdown
. Endpoint /healthz sendiri dibedah pada chapter Health, Readiness, dan Liveness
.
Pool yang dibuka sekali, dipantau, dan ditutup rapi menutup rangkaian section ini. Seluruh bahan — dari query parameter, transaksi, migration, repository, context, testing, hingga koneksi — kini tersedia untuk dipraktikkan utuh pada proyek REST API PostgreSQL .
Referensi #
- Managing connections
— panduan resmi konfigurasi pool
sql.DB. - Package database/sql: DB.Stats
— struktur
sql.DBStatsdan makna tiap field. - Opening a database handle
— perilaku
sql.Opendandb.Ping.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
connection-demo.go
Unduh berkaspackage main
import "fmt"
// Stats meniru sql.DBStats: angka untuk memantau kesehatan pool.
type Stats struct {
MaxOpenConnections int
OpenConnections int
InUse int
WaitCount int64
}
// Pool adalah tiruan *sql.DB: dibuka sekali, dipakai bersama
// seluruh handler, dan ditutup sekali saat aplikasi berhenti.
type Pool struct {
maxOpen int
terpakai int
menunggu int64
ditutup bool
}
// OpenPool membuka pool dengan batas koneksi maksimum.
// sql.Open pada database nyata tidak langsung terhubung;
// koneksi dibuat malas saat query pertama.
func OpenPool(maxOpen int) *Pool {
return &Pool{maxOpen: maxOpen}
}
// CobaAmbil mengambil satu koneksi. Bila pool penuh, pemanggil
// masuk antrean (WaitCount naik) dan permintaan ini ditolak.
func (p *Pool) CobaAmbil() bool {
if p.ditutup {
return false
}
if p.terpakai >= p.maxOpen {
p.menunggu++ // pemanggil menunggu koneksi dikembalikan
return false
}
p.terpakai++
return true
}
// Kembalikan melepas koneksi agar bisa dipakai pemanggil lain.
func (p *Pool) Kembalikan() {
if p.terpakai > 0 {
p.terpakai--
}
}
// Stats melaporkan kondisi pool saat ini.
func (p *Pool) Stats() Stats {
return Stats{
MaxOpenConnections: p.maxOpen,
OpenConnections: p.terpakai,
InUse: p.terpakai,
WaitCount: p.menunggu,
}
}
// Close menutup pool: semua koneksi dilepas dan pool menolak
// permintaan baru. Pada database nyata, Close menutup semua
// koneksi idle sekaligus koneksi yang sedang dipakai.
func (p *Pool) Close() {
p.ditutup = true
}
func main() {
// Inisialisasi sekali di awal, bukan sekali per request.
pool := OpenPool(2)
defer pool.Close() // shutdown: tutup saat program selesai
fmt.Println("ambil pertama :", pool.CobaAmbil())
fmt.Println("ambil kedua :", pool.CobaAmbil())
s := pool.Stats()
fmt.Printf("stats : inUse=%d/%d waitCount=%d\n", s.InUse, s.MaxOpenConnections, s.WaitCount)
// Ambil ketiga: pool penuh, pemanggil menunggu.
fmt.Println("ambil ketiga :", pool.CobaAmbil())
pool.Kembalikan()
s = pool.Stats()
fmt.Printf("stats : inUse=%d/%d waitCount=%d\n", s.InUse, s.MaxOpenConnections, s.WaitCount)
fmt.Println("ambil ulang :", pool.CobaAmbil())
pool.Kembalikan()
pool.Kembalikan()
pool.Close()
fmt.Println("setelah close :", pool.CobaAmbil())
}