Episode ini menghubungkan Gin ke database: memilih driver, koneksi PostgreSQL dengan pgxpool dan connection pooling, migrasi schema dengan golang-migrate, serta implementasi repository pattern dan CRUD lengkap memakai GORM.

API yang hanya menyimpan data di memori tidak akan bertahan lama. Episode 9 ini menghubungkan arsitektur yang sudah kalian susun di episode 8 ke database sungguhan: PostgreSQL. Kalian akan belajar memilih driver, membuat koneksi dengan connection pooling yang sehat, mengelola skema lewat migrasi, dan mengimplementasikan repository pattern dengan CRUD lengkap.
Mengapa topik ini penting? Database adalah sumber kebenaran hampir semua aplikasi bisnis. Koneksi yang dikelola sembarangan akan membuang resource, skema yang tidak termigrasi membuat tim tidak sinkron, dan query yang di-embed di handler menyulitkan pengujian. Lapisan repository menjawab ketiganya.
Go punya beberapa cara terhubung ke PostgreSQL, masing-masing dengan trade-off:
pgxpool.Untuk project yang membutuhkan kecepatan dan kontrol, pgx adalah pilihan utama; untuk produktivitas, GORM unggul. Kalian bisa mengombinasikan keduanya: GORM bahkan bisa berjalan di atas database/sql.
Buka koneksi dari environment, lalu konfigurasi ukuran pool:
func ConnectPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, err
}
cfg.MaxConns = 10
cfg.MinConns = 2
cfg.MaxConnLifetime = 30 * time.Minute
cfg.MaxConnIdleTime = 5 * time.Minute
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, err
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, err
}
return pool, nil
}Konfigurasi cfg.MaxConns = 10 membatasi koneksi konkuren agar tidak menghabiskan resource database. MaxConnLifetime mencegah koneksi basi, dan pool.Ping(ctx) memverifikasi koneksi sebelum dipakai. Panggil pool.Close() saat aplikasi berhenti untuk membersihkan resource.
Migrasi menjaga skema tetap tersimpan sebagai file versioned. Instal CLI golang-migrate, buat file migrasi, lalu jalankan:
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
migrate create -ext sql -dir migrations -seq create_users
ls migrationsexport DATABASE_URL="postgres://arman:rahasia@localhost:5432/belajargin?sslmode=disable"
migrate -path migrations -database "$DATABASE_URL" up
migrate -path migrations -database "$DATABASE_URL" versionFile 000001_create_users.up.sql berisi pernyataan SQL pembuatan tabel, dan pasangannya .down.sql untuk membatalkannya. Perintah migrate -path migrations -database "$DATABASE_URL" up menerapkan semua migrasi yang belum berjalan. Di production, jalankan migrasi di pipeline, bukan di dalam handler.
GORM memetakan struct ke tabel. Buka koneksi dan buat skema otomatis:
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"uniqueIndex"`
CreatedAt time.Time
}
func OpenGORM(dsn string) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
if err := db.AutoMigrate(&User{}); err != nil {
return nil, err
}
return db, nil
}db.AutoMigrate(&User{}) membuat tabel users berikut indeksnya jika belum ada. Untuk skema yang berubah seiring waktu, tetap gunakan migrasi versioned; AutoMigrate hanya untuk kemudahan di pengembangan awal.
Implementasi repository memakai GORM:
type UserRepo struct {
db *gorm.DB
}
func (r *UserRepo) Create(ctx context.Context, u *User) error {
return r.db.WithContext(ctx).Create(u).Error
}
func (r *UserRepo) FindByID(ctx context.Context, id uint) (*User, error) {
var u User
err := r.db.WithContext(ctx).First(&u, id).Error
return &u, err
}
func (r *UserRepo) List(ctx context.Context) ([]User, error) {
var users []User
err := r.db.WithContext(ctx).Find(&users).Error
return users, err
}
func (r *UserRepo) Update(ctx context.Context, u *User) error {
return r.db.WithContext(ctx).Save(u).Error
}
func (r *UserRepo) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&User{}, id).Error
}Semua method menerima ctx dan meneruskannya lewat r.db.WithContext(ctx). Ini penting: ketika request dibatalkan klien, query ikut dibatalkan. Pola WithContext(ctx) menghubungkan siklus hidup database dengan siklus hidup request.
Handler di lapisan atas tinggal memanggil repository yang disuntikkan:
func (h *UserHandler) ListUsers(c *gin.Context) {
users, err := h.repo.List(c.Request.Context())
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"users": users})
}h.repo.List(c.Request.Context()) membuat handler tetap bersih dari SQL. Perhatikan juga bahwa ini mengikuti pola lapisan dari episode 8 — repository dipakai langsung di handler untuk contoh singkat, tetapi di project nyata letakkan di belakang service.
Inti yang harus dibawa pulang:
pgx dengan pgxpool untuk koneksi cepat dan connection pooling yang sehat.golang-migrate menjaga skema tetap tersinkron.ctx agar query dibatalkan saat request dibatalkan.MaxConns, MaxConnLifetime, dan MaxConnIdleTime sesuai beban.Di episode 10 selanjutnya kita akan membedah configuration & environment — membaca variabel lingkungan dengan os.Getenv, godotenv, dan Viper, menyusun config struct yang diinjeksi ke handler, serta implementasi graceful shutdown memakai http.Server dan signal.NotifyContext.