← Semua pembelajaran / Go Nol → Enterprise
Fase 4 · Database & Persistensi

pgx — driver Postgres yang layak dipakai

pgx bisa dipakai sebagai driver database/sql biasa, atau langsung lewat API aslinya yang lebih cepat dan mengerti tipe Postgres. Pilihan itu menentukan seberapa banyak kode yang harus ditulis ulang nanti.

Sumber asli pkg.go.dev Resmi Rangkuman ~7 menit baca

Intisari

  • Dua mode: lewat database/sql (portabel) atau pgxpool langsung (lebih cepat, tipe Postgres asli).
  • Mode asli mendukung uuid, jsonb, array, dan interval tanpa konversi manual.
  • pgx.CollectRows menghapus seluruh boilerplate rows.Next() / Scan.
  • CopyFrom memuat ribuan baris berkali-kali lipat lebih cepat daripada INSERT beruntun.
  • Error Postgres punya kode: 23505 unique violation, 23503 foreign key — petakan jadi error domain, jangan cocokkan teksnya.

Dua mode

database/sql + stdlibpgxpool langsung
Importpgx/v5/stdlibpgx/v5/pgxpool
Bisa ganti database nantiYaTidak
Tipe Postgres asliTerbatasPenuh
PerformaBaikLebih baik (protokol biner)
CopyFrom, batch, LISTEN/NOTIFYTidakYa
Cocok untukYang mungkin pindah databaseYang sudah pasti Postgres

Rekomendasi roadmap ini: pgxpool langsung. "Nanti mungkin pindah ke MySQL" hampir tidak pernah terjadi, dan harganya adalah kehilangan jsonb, array, dan CopyFrom selamanya. Kalau kamu tetap ingin abstraksi, letakkan batasnya di interface repository milikmu sendiri (Fase 1) — bukan di driver.

Menyiapkan pool

import "github.com/jackc/pgx/v5/pgxpool"

func BukaPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
	cfg, err := pgxpool.ParseConfig(dsn)
	if err != nil {
		return nil, fmt.Errorf("parse dsn: %w", err)
	}

	cfg.MaxConns = 25
	cfg.MinConns = 5
	cfg.MaxConnLifetime = 30 * time.Minute
	cfg.MaxConnIdleTime = 5 * time.Minute
	cfg.HealthCheckPeriod = 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, fmt.Errorf("ping: %w", err)
	}
	return pool, nil
}

DSN-nya bisa berupa URL atau pasangan kunci-nilai:

postgres://pengguna:sandi@host:5432/basisdata?sslmode=require&pool_max_conns=25

Di AWS, sslmode tidak boleh disable. RDS mendukung TLS secara bawaan; require adalah minimum, dan verify-full dengan sertifikat CA RDS adalah yang benar untuk data sensitif. Kredensialnya sendiri datang dari Secrets Manager, bukan dari berkas (Fase 10).

Kueri dengan API asli

// Satu baris
var p Produk
err := pool.QueryRow(ctx,
	`SELECT id, nama, harga FROM produk WHERE id = $1`, id,
).Scan(&p.ID, &p.Nama, &p.Harga)

if errors.Is(err, pgx.ErrNoRows) {     // BUKAN sql.ErrNoRows
	return Produk{}, ErrTidakDitemukan
}

// Banyak baris — tanpa loop manual sama sekali
rows, err := pool.Query(ctx,
	`SELECT id, nama, harga FROM produk WHERE harga < $1`, batas)
if err != nil {
	return nil, err
}
hasil, err := pgx.CollectRows(rows, pgx.RowToStructByName[Produk])

CollectRows menghapus seluruh blok defer rows.Close() / for rows.Next() / rows.Err() — termasuk dua baris wajib yang paling sering lupa ditulis. RowToStructByName mencocokkan nama kolom dengan tag db:"..." pada struct. Ini salah satu alasan paling praktis memakai API asli.

Tipe Postgres yang langsung bekerja

type Produk struct {
	ID       uuid.UUID          `db:"id"`
	Nama     string             `db:"nama"`
	Tag      []string           `db:"tag"`        // text[]
	Meta     map[string]any     `db:"meta"`       // jsonb
	Berlaku  pgtype.Range[time.Time] `db:"berlaku"` // tstzrange
	Dihapus  *time.Time         `db:"dihapus_pada"`
}

// jsonb langsung dari map — tanpa Marshal manual
_, err := pool.Exec(ctx,
	`INSERT INTO produk (nama, tag, meta) VALUES ($1, $2, $3)`,
	p.Nama, p.Tag, p.Meta)

Batch dan CopyFrom

// Batch: banyak kueri, SATU perjalanan jaringan
batch := &pgx.Batch{}
for _, p := range produk {
	batch.Queue(`UPDATE produk SET harga = $1 WHERE id = $2`, p.Harga, p.ID)
}
hasil := pool.SendBatch(ctx, batch)
defer hasil.Close()

for range produk {
	if _, err := hasil.Exec(); err != nil {
		return err
	}
}
// CopyFrom: jalur tercepat memuat data massal
_, err := pool.CopyFrom(ctx,
	pgx.Identifier{"produk"},
	[]string{"nama", "harga"},
	pgx.CopyFromSlice(len(produk), func(i int) ([]any, error) {
		return []any{produk[i].Nama, produk[i].Harga}, nil
	}))

Untuk impor data, perbedaannya bukan puluhan persen. Sepuluh ribu INSERT beruntun berarti sepuluh ribu perjalanan bolak-balik jaringan; CopyFrom mengirimkannya sebagai satu aliran. Di lingkungan dengan latensi 1–2 ms per perjalanan — yaitu semua lingkungan AWS — bedanya bisa dari menit jadi detik.

Kode error Postgres

import "github.com/jackc/pgx/v5/pgconn"

func petakanError(err error) error {
	var pg *pgconn.PgError
	if !errors.As(err, &pg) {
		return err
	}

	switch pg.Code {
	case "23505":   // unique_violation
		return fmt.Errorf("%s sudah dipakai: %w", pg.ConstraintName, ErrKonflik)
	case "23503":   // foreign_key_violation
		return fmt.Errorf("referensi tidak valid: %w", ErrValidasiRelasi)
	case "23514":   // check_violation
		return fmt.Errorf("melanggar aturan %s: %w", pg.ConstraintName, ErrValidasi)
	case "40001", "40P01":   // serialization_failure, deadlock_detected
		return ErrCobaLagi
	case "57014":   // query_canceled
		return context.DeadlineExceeded
	}
	return err
}

Ini pengganti cek "sudah ada?" sebelum menyimpan. Memeriksa dulu lalu menyimpan mengandung balapan: dua permintaan bisa sama-sama lolos pemeriksaan. Biarkan unique constraint database yang menjadi kebenaran, lalu terjemahkan kode 23505 jadi 409 Conflict. Satu kueri, tanpa balapan.

Latihan: ganti repository dari materi database/sql ke pgxpool dan pgx.CollectRows, lalu bandingkan jumlah barisnya. Tambahkan unique constraint pada kolom nama, simpan dua produk bernama sama, dan petakan kode 23505 jadi 409 lewat errors.As.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.