MySQL #
Go tidak menyertakan driver database spesifik di standard library — melainkan menyediakan database/sql sebagai interface abstraksi yang seragam. Semua operasi database (query, exec, transaction) dilakukan melalui database/sql, sementara driver spesifik (MySQL, PostgreSQL, SQLite) di-register di balik layar. Ini berarti kode bisnis kamu hampir identik apapun database yang dipakai — hanya connection string dan driver yang berbeda. Untuk MySQL, driver yang paling umum dan matang adalah github.com/go-sql-driver/mysql.
Instalasi #
go get github.com/go-sql-driver/mysql
Koneksi ke MySQL #
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql" // blank import: register driver
)
func main() {
// Format DSN: user:password@protocol(host:port)/dbname?param=value
dsn := "root:password@tcp(localhost:3306)/tokoonline?parseTime=true&loc=Asia%2FJakarta"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal("sql.Open gagal:", err)
}
defer db.Close()
// sql.Open TIDAK membuka koneksi — hanya validasi DSN
// Gunakan Ping untuk memverifikasi koneksi aktif
if err := db.Ping(); err != nil {
log.Fatal("Ping gagal:", err)
}
fmt.Println("Terhubung ke MySQL!")
}
Parameter DSN Penting #
parseTime=true → scan kolom DATETIME/TIMESTAMP ke time.Time (WAJIB)
loc=Asia%2FJakarta → timezone untuk interpretasi waktu
charset=utf8mb4 → support emoji dan karakter Unicode penuh
timeout=10s → connection timeout
readTimeout=30s → read timeout per query
writeTimeout=30s → write timeout per query
multiStatements=true → izinkan beberapa statement dalam satu Exec
Connection Pool — Konfigurasi yang Benar #
database/sql mengelola pool koneksi secara otomatis. Konfigurasi pool sangat penting untuk performa:
func openDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// Jumlah koneksi terbuka maksimum ke server
// Sesuaikan dengan max_connections di MySQL (default 151)
db.SetMaxOpenConns(25)
// Koneksi idle yang dipertahankan di pool
// Sebaiknya sama atau lebih kecil dari MaxOpenConns
db.SetMaxIdleConns(25)
// Maksimum umur koneksi (paksa buat koneksi baru setelah ini)
// Berguna untuk menghindari koneksi stale karena firewall/proxy
db.SetConnMaxLifetime(5 * time.Minute)
// Maksimum waktu koneksi boleh idle di pool (Go 1.15+)
db.SetConnMaxIdleTime(1 * time.Minute)
// Verifikasi pool berfungsi
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("ping database: %w", err)
}
return db, nil
}
Jangan tutup*sql.DBsetelah setiap query.dbadalah pool koneksi yang seharusnya dibuat sekali dan dipakai selama aplikasi berjalan. Panggildb.Close()hanya saat aplikasi berhenti (misalnya dideferdimain()).
Query — Membaca Data #
QueryRowContext — Satu Baris
#
func getProductByID(ctx context.Context, db *sql.DB, id int) (*Product, error) {
query := `
SELECT id, name, price, stock, category, created_at
FROM products
WHERE id = ?
`
row := db.QueryRowContext(ctx, query, id)
var p Product
err := row.Scan(
&p.ID,
&p.Name,
&p.Price,
&p.Stock,
&p.Category,
&p.CreatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound // produk tidak ditemukan
}
return nil, fmt.Errorf("scan product: %w", err)
}
return &p, nil
}
QueryContext — Banyak Baris
#
func listProducts(ctx context.Context, db *sql.DB, category string) ([]*Product, error) {
query := `
SELECT id, name, price, stock, category
FROM products
WHERE category = ?
ORDER BY name ASC
`
rows, err := db.QueryContext(ctx, query, category)
if err != nil {
return nil, fmt.Errorf("query products: %w", err)
}
defer rows.Close() // WAJIB: tutup rows untuk kembalikan koneksi ke pool
var products []*Product
for rows.Next() {
var p Product
if err := rows.Scan(
&p.ID, &p.Name, &p.Price, &p.Stock, &p.Category,
); err != nil {
return nil, fmt.Errorf("scan row: %w", err)
}
products = append(products, &p)
}
// Cek error setelah iterasi selesai
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows error: %w", err)
}
return products, nil
}
defer rows.Close()wajib dipanggil. Jika rows tidak ditutup, koneksi tidak dikembalikan ke pool dan pool bisa habis. Panggil segera setelahQueryContext— bahkan jika belum iterasi sama sekali.
Exec — Menulis Data #
ExecContext untuk INSERT, UPDATE, DELETE — tidak mengembalikan rows:
// INSERT
func createProduct(ctx context.Context, db *sql.DB, p *Product) (int64, error) {
query := `
INSERT INTO products (name, price, stock, category, created_at)
VALUES (?, ?, ?, ?, NOW())
`
result, err := db.ExecContext(ctx, query,
p.Name, p.Price, p.Stock, p.Category,
)
if err != nil {
return 0, fmt.Errorf("insert product: %w", err)
}
// Ambil ID yang baru dibuat
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("last insert id: %w", err)
}
return id, nil
}
// UPDATE
func updateProduct(ctx context.Context, db *sql.DB, p *Product) error {
query := `
UPDATE products
SET name = ?, price = ?, stock = ?, category = ?
WHERE id = ?
`
result, err := db.ExecContext(ctx, query,
p.Name, p.Price, p.Stock, p.Category, p.ID,
)
if err != nil {
return fmt.Errorf("update product: %w", err)
}
// Cek apakah ada baris yang terpengaruh
rows, _ := result.RowsAffected()
if rows == 0 {
return ErrNotFound
}
return nil
}
// DELETE
func deleteProduct(ctx context.Context, db *sql.DB, id int) error {
result, err := db.ExecContext(ctx,
"DELETE FROM products WHERE id = ?", id,
)
if err != nil {
return fmt.Errorf("delete product: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return ErrNotFound
}
return nil
}
Prepared Statement #
Prepared statement diparse sekali oleh server, bisa dieksekusi berkali-kali dengan parameter berbeda — lebih aman dari SQL injection dan lebih efisien untuk query berulang:
// Buat statement sekali, pakai berkali-kali
func bulkUpdateStock(ctx context.Context, db *sql.DB, updates []StockUpdate) error {
stmt, err := db.PrepareContext(ctx,
"UPDATE products SET stock = ? WHERE id = ?",
)
if err != nil {
return fmt.Errorf("prepare statement: %w", err)
}
defer stmt.Close() // kembalikan statement ke pool
for _, u := range updates {
if _, err := stmt.ExecContext(ctx, u.NewStock, u.ProductID); err != nil {
return fmt.Errorf("update stok produk %d: %w", u.ProductID, err)
}
}
return nil
}
Transaksi #
Transaksi memastikan sekelompok operasi berjalan secara atomik — semua berhasil atau semua dibatalkan:
func transferStock(ctx context.Context, db *sql.DB, fromID, toID, qty int) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
// Defer rollback — tidak berbahaya jika commit sudah berhasil
defer tx.Rollback()
// Kurangi stok dari produk asal
result, err := tx.ExecContext(ctx,
"UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?",
qty, fromID, qty,
)
if err != nil {
return fmt.Errorf("kurangi stok: %w", err)
}
if rows, _ := result.RowsAffected(); rows == 0 {
return errors.New("stok tidak mencukupi")
}
// Tambah stok ke produk tujuan
if _, err := tx.ExecContext(ctx,
"UPDATE products SET stock = stock + ? WHERE id = ?",
qty, toID,
); err != nil {
return fmt.Errorf("tambah stok: %w", err)
}
// Catat log transfer
if _, err := tx.ExecContext(ctx,
"INSERT INTO stock_transfers (from_id, to_id, qty, created_at) VALUES (?, ?, ?, NOW())",
fromID, toID, qty,
); err != nil {
return fmt.Errorf("catat transfer: %w", err)
}
// Commit — jika berhasil, defer Rollback tidak berpengaruh
return tx.Commit()
}
Null Values #
MySQL memungkinkan kolom bernilai NULL. Gunakan tipe sql.Null* untuk menanganinya:
type Product struct {
ID int
Name string
Description sql.NullString // bisa NULL
Price float64
DeletedAt sql.NullTime // soft delete, bisa NULL
Weight sql.NullFloat64 // bisa NULL
}
// Scan dengan null values
var p Product
err := row.Scan(
&p.ID,
&p.Name,
&p.Description, // auto-handle NULL
&p.Price,
&p.DeletedAt,
&p.Weight,
)
// Akses nilai
if p.Description.Valid {
fmt.Println("Deskripsi:", p.Description.String)
} else {
fmt.Println("Tidak ada deskripsi")
}
// Insert dengan null
description := sql.NullString{} // NULL
if desc := "Laptop gaming"; desc != "" {
description = sql.NullString{String: desc, Valid: true}
}
db.ExecContext(ctx,
"INSERT INTO products (name, description) VALUES (?, ?)",
"Laptop", description,
)
Batch Insert #
Untuk insert banyak baris sekaligus, hindari loop satu per satu:
func batchInsertProducts(ctx context.Context, db *sql.DB, products []Product) error {
if len(products) == 0 {
return nil
}
// Bangun query dengan banyak placeholder
// INSERT INTO products (name, price, stock) VALUES (?, ?, ?), (?, ?, ?), ...
valueStrings := make([]string, len(products))
valueArgs := make([]interface{}, 0, len(products)*3)
for i, p := range products {
valueStrings[i] = "(?, ?, ?)"
valueArgs = append(valueArgs, p.Name, p.Price, p.Stock)
}
query := fmt.Sprintf(
"INSERT INTO products (name, price, stock) VALUES %s",
strings.Join(valueStrings, ","),
)
_, err := db.ExecContext(ctx, query, valueArgs...)
if err != nil {
return fmt.Errorf("batch insert: %w", err)
}
return nil
}
Contoh Program Lengkap — Repository Pattern #
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
)
var ErrNotFound = errors.New("data tidak ditemukan")
type Product struct {
ID int
Name string
Price float64
Stock int
Category string
CreatedAt time.Time
}
// ProductRepository — interface untuk abstraksi
type ProductRepository interface {
Create(ctx context.Context, p *Product) (int64, error)
FindByID(ctx context.Context, id int) (*Product, error)
FindByCategory(ctx context.Context, category string) ([]*Product, error)
Update(ctx context.Context, p *Product) error
Delete(ctx context.Context, id int) error
Search(ctx context.Context, keyword string, limit int) ([]*Product, error)
}
// mysqlProductRepo — implementasi MySQL
type mysqlProductRepo struct {
db *sql.DB
}
func NewProductRepository(db *sql.DB) ProductRepository {
return &mysqlProductRepo{db: db}
}
func (r *mysqlProductRepo) Create(ctx context.Context, p *Product) (int64, error) {
result, err := r.db.ExecContext(ctx, `
INSERT INTO products (name, price, stock, category, created_at)
VALUES (?, ?, ?, ?, NOW())
`, p.Name, p.Price, p.Stock, p.Category)
if err != nil {
return 0, fmt.Errorf("create product: %w", err)
}
return result.LastInsertId()
}
func (r *mysqlProductRepo) FindByID(ctx context.Context, id int) (*Product, error) {
var p Product
err := r.db.QueryRowContext(ctx, `
SELECT id, name, price, stock, category, created_at
FROM products WHERE id = ?
`, id).Scan(&p.ID, &p.Name, &p.Price, &p.Stock, &p.Category, &p.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("find product %d: %w", id, err)
}
return &p, nil
}
func (r *mysqlProductRepo) FindByCategory(ctx context.Context, category string) ([]*Product, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, name, price, stock, category, created_at
FROM products WHERE category = ? ORDER BY name
`, category)
if err != nil {
return nil, fmt.Errorf("list products: %w", err)
}
defer rows.Close()
var products []*Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock,
&p.Category, &p.CreatedAt); err != nil {
return nil, fmt.Errorf("scan product: %w", err)
}
products = append(products, &p)
}
return products, rows.Err()
}
func (r *mysqlProductRepo) Update(ctx context.Context, p *Product) error {
res, err := r.db.ExecContext(ctx, `
UPDATE products SET name=?, price=?, stock=?, category=?
WHERE id=?
`, p.Name, p.Price, p.Stock, p.Category, p.ID)
if err != nil {
return fmt.Errorf("update product: %w", err)
}
if n, _ := res.RowsAffected(); n == 0 {
return ErrNotFound
}
return nil
}
func (r *mysqlProductRepo) Delete(ctx context.Context, id int) error {
res, err := r.db.ExecContext(ctx,
"DELETE FROM products WHERE id = ?", id)
if err != nil {
return fmt.Errorf("delete product: %w", err)
}
if n, _ := res.RowsAffected(); n == 0 {
return ErrNotFound
}
return nil
}
func (r *mysqlProductRepo) Search(ctx context.Context, keyword string, limit int) ([]*Product, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, name, price, stock, category, created_at
FROM products
WHERE name LIKE ? OR category LIKE ?
ORDER BY name LIMIT ?
`, "%"+keyword+"%", "%"+keyword+"%", limit)
if err != nil {
return nil, fmt.Errorf("search products: %w", err)
}
defer rows.Close()
var products []*Product
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock,
&p.Category, &p.CreatedAt); err != nil {
return nil, err
}
products = append(products, &p)
}
return products, rows.Err()
}
// DDL untuk setup tabel
const createTableSQL = `
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(15,2) NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
category VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_category (category),
INDEX idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
func main() {
dsn := "root:password@tcp(localhost:3306)/tokoonline?parseTime=true"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
if err := db.Ping(); err != nil {
log.Fatal("Koneksi gagal:", err)
}
// Setup tabel
if _, err := db.Exec(createTableSQL); err != nil {
log.Fatal("Create table:", err)
}
ctx := context.Background()
repo := NewProductRepository(db)
// Buat produk
products := []Product{
{Name: "Laptop Pro 14", Price: 15_000_000, Stock: 10, Category: "elektronik"},
{Name: "Mouse Wireless", Price: 350_000, Stock: 50, Category: "elektronik"},
{Name: "Keyboard Mech", Price: 1_500_000, Stock: 25, Category: "elektronik"},
{Name: "Kaos Polos", Price: 85_000, Stock: 100, Category: "fashion"},
{Name: "Celana Chino", Price: 250_000, Stock: 60, Category: "fashion"},
}
fmt.Println("=== Insert Produk ===")
ids := make([]int64, 0)
for _, p := range products {
p := p
id, err := repo.Create(ctx, &p)
if err != nil {
log.Printf("Gagal insert %s: %v", p.Name, err)
continue
}
ids = append(ids, id)
fmt.Printf(" [%d] %s — Rp%.0f\n", id, p.Name, p.Price)
}
// Cari satu produk
fmt.Println("\n=== FindByID ===")
if len(ids) > 0 {
p, err := repo.FindByID(ctx, int(ids[0]))
if err != nil {
log.Println("FindByID error:", err)
} else {
fmt.Printf(" ID=%d, %s, Rp%.0f, Stok=%d\n",
p.ID, p.Name, p.Price, p.Stock)
}
}
// Daftar per kategori
fmt.Println("\n=== FindByCategory: elektronik ===")
elektronik, _ := repo.FindByCategory(ctx, "elektronik")
for _, p := range elektronik {
fmt.Printf(" [%d] %-20s Rp%10.0f stok=%d\n",
p.ID, p.Name, p.Price, p.Stock)
}
// Update
fmt.Println("\n=== Update ===")
if len(ids) > 0 {
err := repo.Update(ctx, &Product{
ID: int(ids[0]), Name: "Laptop Pro 14 (New)",
Price: 14_500_000, Stock: 8, Category: "elektronik",
})
if err != nil {
log.Println("Update error:", err)
} else {
fmt.Println(" Update berhasil")
}
}
// Search
fmt.Println("\n=== Search: 'laptop' ===")
results, _ := repo.Search(ctx, "laptop", 10)
for _, p := range results {
fmt.Printf(" [%d] %s — Rp%.0f\n", p.ID, p.Name, p.Price)
}
// Delete
if len(ids) > 0 {
fmt.Println("\n=== Delete ===")
err := repo.Delete(ctx, int(ids[len(ids)-1]))
if err != nil {
log.Println("Delete error:", err)
} else {
fmt.Printf(" Produk ID %d dihapus\n", ids[len(ids)-1])
}
}
// Statistik pool
stats := db.Stats()
fmt.Printf("\n=== Pool Stats ===\n")
fmt.Printf(" Open connections : %d\n", stats.OpenConnections)
fmt.Printf(" In use : %d\n", stats.InUse)
fmt.Printf(" Idle : %d\n", stats.Idle)
fmt.Printf(" Wait count : %d\n", stats.WaitCount)
_ = strings.Join // suppress import
}
Ringkasan #
database/sqladalah abstraksi standar; driver di-import dengan blank import_ "github.com/go-sql-driver/mysql".- DSN wajib mengandung
parseTime=trueagar kolomDATETIME/TIMESTAMPotomatis di-scan ketime.Time.- Connection pool:
SetMaxOpenConns,SetMaxIdleConns,SetConnMaxLifetimewajib dikonfigurasi — default tidak membatasi koneksi.defer rows.Close()segera setelahQueryContext— jika tidak, koneksi tidak kembali ke pool.sql.ErrNoRowsdariQueryRow.Scanberarti baris tidak ditemukan — bukan error fatal.result.RowsAffected()untuk UPDATE/DELETE — cek apakah ada baris yang benar-benar terpengaruh.- Prepared statement untuk query yang dieksekusi berulang kali — lebih aman dan efisien.
- Transaksi dengan
defer tx.Rollback()— aman karena Rollback setelah Commit tidak berbahaya.sql.NullString,sql.NullTimeuntuk kolom yang bisa NULL — cek fieldValidsebelum mengakses nilainya.- Batch insert dengan satu query multi-values jauh lebih cepat daripada loop
INSERTsatu per satu.