Go terkenal dengan standar pustaka database/sql: satu antarmuka untuk SQL apa pun driver-nya. Masalahnya, database/sql tidak tahu apa-apa tentang PostgreSQL — fitur khas seperti LISTEN/NOTIFY atau COPY tidak terjangkau lewat antarmuka itu. Ekosistem Go menjawab dengan pgx, driver yang mengimplementasikan protokol PostgreSQL secara langsung dan kini menjadi pilihan de facto untuk aplikasi produksi. Chapter ini memetakan pola idiomatik pgx v5 untuk kebutuhan Toko Maju Jaya: koneksi lewat pool, query satu baris dan banyak baris, transaksi dengan context, serta penanganan error berbasis SQLSTATE — pola yang sama dengan chapter PHP sebelumnya, kali ini dengan gaya Go.

pgx: Driver de Facto untuk Go #

pgx (paket github.com/jackc/pgx/v5) hadir dalam dua wajah. Wajah pertama adalah driver untuk database/sql: Anda menulis kode database/sql standar dan pgx bekerja di belakangnya. Wajah kedua adalah API pgx native yang menembak protokol PostgreSQL langsung — lebih cepat dan lebih kaya fitur, dengan biaya terikat pada satu database. Keduanya berbagi kode protokol yang sama, jadi pilihan bukan soal performa mentah melainkan soal fleksibilitas API:

Aspekdatabase/sql + pgx stdlibAPI native pgx / pgxpool
Antarmukastandar Go, mudah diganti driverkhusus PostgreSQL
Fitur khusus PostgreSQLterbataspenuh (COPY, LISTEN/NOTIFY, tipe array)
Bentuk koneksi*sql.DB (pool bawaan)*pgxpool.Pool (pool sendiri)
Cocok untukkode yang mungkin ganti DBaplikasi yang komit ke PostgreSQL

Contoh pada chapter ini memakai API native lewat pgxpool, karena itu jalur yang direkomendasikan proyek pgx untuk aplikasi baru.

String Koneksi #

pgx menerima string koneksi dalam dua bentuk — keduanya juga dikenali libpq, jadi kebiasaan dari psql terbawa:

go
// Bentuk URL:
dsn := "postgres://postgres:YOUR_PASSWORD@localhost:5432/toko_db"

// Bentuk kata kunci (sama seperti libpq):
dsn := "host=localhost port=5432 user=postgres " +
    "password=YOUR_PASSWORD dbname=toko_db"

Kata sandi di URL harus di-escape bila mengandung karakter khusus (@, :, /), sehingga bentuk kata kunci lebih aman untuk kredensial yang rumit. Sama seperti PDO di chapter sebelumnya, parameter yang diisi ulang oleh variabel environment libpq (PGHOST, PGPORT, PGDATABASE) cukup dibiarkan kosong di string koneksi.

Bahan Latihan: Tabel Pesanan #

Contoh-contoh berikutnya bekerja di atas tabel pesanan Toko Maju Jaya. File berikut menjalankan DDL dan data awal lewat psql:

-- Bahan latihan chapter Go: tabel pesanan Toko Maju Jaya.
-- Tabel ini dibuat dengan psql; seluruh contoh kode Go pada
-- chapter ini bekerja di atas tabel yang sama.
DROP TABLE IF EXISTS pesanan;

CREATE TABLE pesanan (
    id           integer PRIMARY KEY,
    nama_pemesan text NOT NULL,
    total        bigint NOT NULL CHECK (total >= 0),
    status       text NOT NULL DEFAULT 'baru'
);

INSERT INTO pesanan (id, nama_pemesan, total, status) VALUES
    (101, 'Budi', 835000, 'dikirim'),
    (102, 'Siti', 1850000, 'baru'),
    (103, 'Ani', 65000, 'selesai');

Output yang Dihasilkan:

text
DROP TABLE
CREATE TABLE
INSERT 0 3

Koneksi dengan pgxpool #

Gateway aplikasi biasanya berupa satu pgxpool.Pool yang dibuat sekali saat program mulai, dibagikan ke seluruh handler, dan ditutup saat program berhenti:

go
package main

import (
	"context"
	"log"

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

func main() {
	ctx := context.Background()

	pool, err := pgxpool.New(ctx,
		"postgres://postgres:YOUR_PASSWORD@localhost:5432/toko_db")
	if err != nil {
		log.Fatal(err)
	}
	defer pool.Close()

	// seluruh contoh query di chapter ini memakai `pool` ini
}

pgxpool.New hanya memvalidasi string koneksi; koneksi server benar-benar dibuka saat pertama kali dibutuhkan, lalu dipelihara dan didaur ulang di dalam pool. Detail perilaku pool — dan mengapa ukurannya sebaiknya kecil — dibahas penuh di chapter Database Connection Pooling .

Satu Baris: QueryRow dan ErrNoRows #

Query yang diharapkan mengembalikan tepat satu baris dipanggil lewat QueryRow, lalu hasilnya disalin ke variabel Go lewat Scan. Bila baris tidak ada, Scan mengembalikan pgx.ErrNoRows — error sentinel yang diperiksa dengan errors.Is:

go
var nama string
var total int64

err := pool.QueryRow(ctx,
	"SELECT nama_pemesan, total FROM pesanan WHERE id = $1", 102,
).Scan(&nama, &total)
if errors.Is(err, pgx.ErrNoRows) {
	fmt.Println("Pesanan tidak ditemukan")
	return
}
if err != nil {
	log.Fatal(err)
}
fmt.Printf("%s membayar Rp%d\n", nama, total)

Dua hal layak diperhatikan. Penanda parameter selalu $1, $2, … — penulisan PostgreSQL, bukan ? ala MySQL. Jumlah argumen Scan harus sama dengan jumlah kolom yang dipilih; kalau tidak, Scan mengembalikan error.

Banyak Baris: Query, Scan, rows.Err #

Untuk banyak baris, Query mengembalikan pgx.Rows yang diiterasi dengan Next dan wajib ditutup. Pola lengkapnya punya empat bagian yang tidak boleh ada yang tertinggal:

go
rows, err := pool.Query(ctx,
	"SELECT id, nama_pemesan, total FROM pesanan WHERE status = $1 ORDER BY id",
	"baru",
)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
	var id int
	var nama string
	var total int64
	if err := rows.Scan(&id, &nama, &total); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d %s Rp%d\n", id, nama, total)
}
if err := rows.Err(); err != nil {
	log.Fatal(err)
}
  1. defer rows.Close() — menutup rows sejak awal menjamin koneksi kembali ke pool di semua jalur keluar, termasuk return di tengah loop.
  2. rows.Next() — memajukan iterator dan memuat baris berikutnya; loop berhenti otomatis saat data habis.
  3. rows.Scan(...) di dalam loop — menyalin kolom baris saat ini ke variabel; kesalahan jumlah atau tipe terdeteksi di sini.
  4. rows.Err() setelah loop — error jaringan di tengah pengiriman baris tidak menghentikan Next dengan panic; error itu dibawa sampai akhir dan hanya terbaca lewat rows.Err(). Melewatkan pemeriksaan ini berarti menelan kegagalan diam-diam.

Exec dan Jumlah Baris Terpengaruh #

INSERT, UPDATE, dan DELETE dipanggil lewat Exec yang mengembalikan pgconn.CommandTag — label perintah beserta jumlah barisnya:

go
tag, err := pool.Exec(ctx,
	"UPDATE pesanan SET status = $1 WHERE id = $2", "dikirim", 102,
)
if err != nil {
	log.Fatal(err)
}
fmt.Println("Baris terpengaruh:", tag.RowsAffected())

tag.String() berisi teks mentah dari server (misalnya UPDATE 1), sedangkan tag.RowsAffected() menyarikan angkanya. Untuk membaca data baris yang barusan ditulis, gunakan RETURNING dengan QueryRow — sama seperti pola PDO di chapter PHP.

Transaksi: Begin, Commit, defer Rollback #

Transaksi pgx dimulai dengan pool.Begin(ctx) yang mengembalikan pgx.Tx. Seluruh operasi dalam transaksi berjalan lewat objek tx itu, bukan lewat pool. Pola idiomatiknya memakai satu defer:

go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

tx, err := pool.Begin(ctx)
if err != nil {
	log.Fatal(err)
}
// Rollback setelah Commit mengembalikan pgx.ErrTxClosed tanpa
// efek samping, jadi defer ini aman di semua jalur.
defer tx.Rollback(ctx)

if _, err := tx.Exec(ctx,
	"UPDATE pesanan SET status = $1 WHERE id = $2", "selesai", 103,
); err != nil {
	log.Fatal(err) // defer Rollback ikut dijalankan
}
if err := tx.Commit(ctx); err != nil {
	log.Fatal(err)
}

Urutan kejadiannya: Begin membuka transaksi, tx.Exec menjalankan pernyataan di dalamnya, dan tx.Commit menutupnya dengan menjadikan perubahan permanen. Baris defer tx.Rollback(ctx) tampak aneh bagi pendatang dari bahasa lain — rollback selalu dipanggil? Jawabannya: bila Commit sudah sukses, Rollback menjadi no-op yang hanya mengembalikan error kecil; bila kode gagal di tengah jalan, rollback dijamin terjadi tanpa perlu menulisnya ulang di setiap cabang error.

Error PostgreSQL: pgconn.PgError #

Error dari server PostgreSQL tiba sebagai *pgconn.PgError dan diperiksa dengan errors.As. Struct tersebut memuat field protokol error: Code (SQLSTATE), Message, Detail, ConstraintName:

go
_, err := pool.Exec(ctx,
	"INSERT INTO pesanan (id, nama_pemesan, total) VALUES ($1, $2, $3)",
	101, "Budi", 250000,
)
if err != nil {
	var pgErr *pgconn.PgError
	if errors.As(err, &pgErr) && pgErr.Code == "23505" {
		fmt.Println("Nomor pesanan sudah dipakai:", pgErr.Message)
		return
	}
	log.Fatal(err)
}

SQLSTATE 23505 (unique_violation) adalah kode yang sama dengan yang ditangkap PDO lewat errorInfo[0] — standar lintas driver yang dibedah penuh di chapter menangani error database. Perhatikan errors.As dipakai untuk membongkar error bertipe, sedangkan errors.Is dipakai untuk membandingkan error sentinel seperti pgx.ErrNoRows; keduanya tidak saling menggantikan.

context: Batas Waktu per Operasi #

Setiap panggilan pgx menerima context.Context sebagai argumen pertama. Lewat mekanisme inilah Go membatalkan operasi database: context.WithTimeout menjatuhkan batas waktu, dan pembatalan menyebarkan sinyal sampai query yang sedang berjalan di server. Praktik umumnya satu context per request HTTP turunan dari request handler — query lambat ikut dibatalkan saat klien menutup halaman, bukan menggantung sampai selesai sendiri.

Dari Go kita berpindah ke bahasa yang paling sering dipakai bersama PostgreSQL di dunia data: Python. Chapter berikutnya menguji psycopg 3 secara nyata di server latihan — koneksi, parameter %s, transaksi dengan context manager, sampai penanganan error — dengan output yang benar-benar dieksekusi, bukan sekadar didokumentasikan.

Referensi #

Berkas Sumber

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

siapkan-tabel.sql

Unduh berkas
sql
-- Bahan latihan chapter Go: tabel pesanan Toko Maju Jaya.
-- Tabel ini dibuat dengan psql; seluruh contoh kode Go pada
-- chapter ini bekerja di atas tabel yang sama.
DROP TABLE IF EXISTS pesanan;

CREATE TABLE pesanan (
    id           integer PRIMARY KEY,
    nama_pemesan text NOT NULL,
    total        bigint NOT NULL CHECK (total >= 0),
    status       text NOT NULL DEFAULT 'baru'
);

INSERT INTO pesanan (id, nama_pemesan, total, status) VALUES
    (101, 'Budi', 835000, 'dikirim'),
    (102, 'Siti', 1850000, 'baru'),
    (103, 'Ani', 65000, 'selesai');