Bahasa kedua di section ini mengambil jalan yang berbeda dari PHP. Go tidak memiliki ekstensi khusus MySQL di pustaka standarnya; sebagai gantinya, Go menyediakan satu antarmuka generik bernama database/sql yang menyeragamkan akses ke semua database relasional, lalu membiarkan driver pihak ketiga mengisi detail protokolnya. Dalam chapter ini kita memasang driver MySQL resmi, memahami format DSN-nya, membuktikan bahwa sql.Open ternyata malas dan tidak langsung terhubung, lalu menulis pola lengkap Query-Scan-rows.Err() beserta prepared statement — pola yang menjadi tulang punggung hampir semua aplikasi Go berdatabase.
Satu Antarmuka, Banyak Driver #
Paket database/sql mendefinisikan tipe dan fungsi umum — sql.DB, sql.Rows, sql.Stmt — tanpa tahu apa pun tentang protokol MySQL. Kode aplikasi kita hanya berbicara dengan tipe-tipe umum tersebut; koneksi sebenarnya dibuka oleh driver yang mendaftarkan dirinya lewat init ketika diimpor. Driver MySQL paling banyak dipakai adalah github.com/go-sql-driver/mysql, dipasang dengan:
go get github.com/go-sql-driver/mysqlKarena driver cukup mendaftarkan dirinya tanpa perlu dipanggil langsung, ia diimpor dengan blank identifier _ — pola yang membuat pembaca Go selalu mengenali baris ini sebagai “saya memakai database/sql dengan driver MySQL”:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)DSN dan sql.Open yang Malas #
Koneksi dikonfigurasi lewat satu string DSN (Data Source Name) dengan format user:password@tcp(host:port)/database. Untuk server latihan kita — user root, password kosong, port 3306 — DSN-nya menjadi root:@tcp(127.0.0.1:3306)/belajar_app. Perhatikan tanda titik dua tetap hadir meski password kosong: root: lalu @.
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
dsn := "root:@tcp(127.0.0.1:3306)/belajar_app"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatal(err)
}
fmt.Println("koneksi siap")
}Bedah baris-per-barisnya:
- Baris
dsn := ...merangkum host, port, user, password, dan database dalam satu string. Password kosong ditulis apa adanya di antara:dan@; bila nanti kita memakai user khusus dengan password, string menjadi misalnyaapp_user:rahasia@tcp(127.0.0.1:3306)/belajar_app. sql.Open("mysql", dsn)hanya memvalidasi format DSN dan menyiapkan objek*sql.DB— pernyataan ini tidak membuka koneksi jaringan sama sekali. Kesalahan klasik pemula: mengira koneksi sudah hidup padahal server bisa saja mati.db.Ping()adalah cara jujur memastikan server benar-benar terjangkau; panggilan inilah yang pertama kali membuka koneksi sungguhan.defer db.Close()menjadwalkan penutupan saat fungsi selesai — idiom Go yang memastikan sumber daya lepas meski fungsi keluar lebih awal lewatreturnataulog.Fatal.
Objek *sql.DB juga bukan satu koneksi: pustaka database/sql mengelola kumpulan koneksi di belakangnya — kita bahas penuh di chapter Database Connection Pooling.
Membaca Banyak Baris: Query, Scan, rows.Err #
Data latihan kita bangun dulu di klien, lalu query patokan dijalankan dengan nilai konkret — outputnya akan kita bandingkan dengan hasil kode Go:
DROP DATABASE IF EXISTS belajar_app;
CREATE DATABASE belajar_app;
USE belajar_app;
CREATE TABLE produk (
id INT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
harga INT NOT NULL
);
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Kopi Toraja', 52000),
(2, 'Teh Melati', 28000),
(3, 'Gula Aren', 32000);Query yang dikirim dari Go memakai placeholder ? untuk filter harga; di klien kita jalankan bentuk konkret-nya dengan nilai 40000:
SELECT id, nama, harga FROM produk WHERE harga < 40000 ORDER BY id;Output yang Dihasilkan:
+----+------------+-------+
| id | nama | harga |
+----+------------+-------+
| 2 | Teh Melati | 28000 |
| 3 | Gula Aren | 32000 |
+----+------------+-------+
2 rows in set (0.00 sec)Dengan hasil patokan itu di tangan, berikut pola baca lengkap di Go:
rows, err := db.Query("SELECT id, nama, harga FROM produk WHERE harga < ? ORDER BY id", 40000)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id, harga int
var nama string
if err := rows.Scan(&id, &nama, &harga); err != nil {
log.Fatal(err)
}
fmt.Printf("%d | %s | %d\n", id, nama, harga)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}Output yang Dihasilkan (kode ilustratif, sesuai SELECT patokan di atas):
2 | Teh Melati | 28000
3 | Gula Aren | 32000Lima bagian pola ini wajib dipahami berurutan:
db.Query(sql, args...)mengirim query dengan placeholder?yang diisi argumen berikutnya secara berurutan — driver memastikan nilai dikirim sebagai data, bukan digabung ke teks SQL.rows.Close()dilepas lewatdefersegera setelah query sukses. Selamarowsmasih terbuka, koneksi yang menanganinya terkunci untuk streaming; lupa menutupnya adalah cara paling umum menghabiskan pool.rows.Next()memajukan kursor satu baris dan mengembalikanfalsesaat data habis — perulangan ini menggantikanwhile fetchala PHP.rows.Scan(&id, &nama, &harga)menyalin kolom baris saat ini ke variabel, urutan pointer mengikuti urutan kolom diSELECT. Tipe tujuan harus cocok: kolomINTdiscan keint, kolomVARCHARkestring; kolom yang bisaNULLharus discan kesql.NullStringatau pointer.rows.Err()diperiksa setelah perulangan selesai.rows.Next()mengembalikanfalsebaik karena data habis maupun karena terjadi kesalahan di tengah jalan; hanyarows.Err()yang bisa membedakannya.
Satu Baris dan Statement Tanpa Hasil #
Untuk query yang diketahui menghasilkan satu baris, QueryRow memadatkan pola di atas:
var harga int
err := db.QueryRow("SELECT harga FROM produk WHERE id = ?", 1).Scan(&harga)
switch {
case err == sql.ErrNoRows:
fmt.Println("produk tidak ditemukan")
case err != nil:
log.Fatal(err)
default:
fmt.Printf("harga produk: %d\n", harga)
}QueryRow menutup barisnya sendiri — tidak ada defer Close — dan kesalahan “baris tidak ada” hadir sebagai sql.ErrNoRows, nilai sentinel yang lazim diperiksa dengan switch. Untuk INSERT, UPDATE, dan DELETE, kita memakai Exec yang mengembalikan ringkasan efek:
hasil, err := db.Exec("INSERT INTO produk (id, nama, harga) VALUES (?, ?, ?)", 4, "Susu Kental", 38000)
if err != nil {
log.Fatal(err)
}
jumlah, err := hasil.RowsAffected()
if err != nil {
log.Fatal(err)
}
fmt.Printf("baris ditambahkan: %d\n", jumlah)RowsAffected() mengembalikan bilangan bulat 64-bit sesuai jumlah baris yang benar-benar berubah — angka yang sama dengan pesan N rows affected yang selama ini kita lihat di klien.
Prepared Statement #
Bila satu query dieksekusi berulang kali dengan nilai berbeda, Prepare membuat statement tersimpan sekali lalu dipakai berkali-kali:
stmt, err := db.Prepare("SELECT nama FROM produk WHERE id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
var nama string
for _, id := range []int{1, 2, 3} {
if err := stmt.QueryRow(id).Scan(&nama); err != nil {
log.Fatal(err)
}
fmt.Printf("id %d: %s\n", id, nama)
}db.Prepare mengirim teks SQL sekali; setiap stmt.QueryRow/stmt.Exec hanya mengirim nilai parameternya. Chapter Prepared Statements dalam Aplikasi membedah perbedaan ini dari sisi protokol dan menjelaskan kapan optimasi ini layak dilakukan.
Membersihkan Latihan #
DROP DATABASE IF EXISTS belajar_app;Referensi #
- go-sql-driver/mysql — GitHub
- Connection Management — MySQL 8.4 Reference Manual
- SQL Prepared Statements — MySQL 8.4 Reference Manual
Kontras berikutnya tajam: Python menyediakan dua driver resmi sekaligus dengan gaya API yang jauh lebih ringkas — dan yang membuat spesial, salah satunya terpasang di lingkungan latihan ini sehingga contoh kode berikutnya berjalan sungguhan. Mari lanjut ke chapter MySQL dan Python.