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:

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

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

bash
go run connection-demo.go

Output yang Dihasilkan:

text
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 : false

Mari kita telaah bagian-bagian penting berkas tersebut:

  1. Baris 25-27 (OpenPool): pool dibuat sekali dengan batas dua koneksi — pengganti sql.Open beserta konfigurasinya.
  2. Baris 31-41 (CobaAmbil): pengambilan koneksi. Bila pool penuh, menunggu naik (tiruan WaitCount) dan permintaan ditolak; sql.DB nyata membiarkan pemanggil menunggu sampai koneksi tersedia atau ctx dibatalkan.
  3. Baris 43-48 (Kembalikan): melepas koneksi — setara rows.Close() atau selesainya transaksi yang mengembalikan koneksi ke pool.
  4. Baris 50-58 (Stats): laporan kondisi pool pada satu titik waktu, pengganti db.Stats().
  5. Baris 67-70 (main): pola inti chapter ini — pool dibuka sekali di awal main, dibagikan lewat variabel, dan defer pool.Close() menutupnya saat program selesai.
  6. Baris 79-85 (pool penuh): percobaan ketiga ditolak dan waitCount naik 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:

MethodPeranNilai awal yang wajar
SetMaxOpenConns(n)Batas total koneksi10-25, sesuai kapasitas database
SetMaxIdleConns(n)Koneksi idle yang disimpanSama dengan MaxOpen untuk beban stabil
SetConnMaxLifetime(d)Umur maksimum satu koneksi30 menit, agar DNS/load-balancer tidak basi
SetConnMaxIdleTime(d)Umur koneksi yang menganggur5-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:

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

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

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

connection-demo.go

Unduh berkas
go
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())
}