這是 Go 語言從零到 Web 應用系列的第十五篇。前面我們用 SQLite 作為開發資料庫,現在升級到生產環境常用的 PostgreSQL,學習進階的資料庫功能。

用 Docker Compose 啟動 PostgreSQL

不需要在本地安裝 PostgreSQL,用 Docker 最方便:

# docker-compose.yml
version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: bookstore
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
docker compose up -d

連線字串:postgres://app:secret@localhost:5432/bookstore?sslmode=disable

pgx Driver

pgx 是目前 Go 生態中效能最好的 PostgreSQL driver,比傳統的 lib/pq 快且功能更完整。

安裝:

go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool

基本連線

package main

import (
    "context"
    "fmt"
    "log"

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

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

    // pgxpool 自帶連線池
    pool, err := pgxpool.New(ctx, "postgres://app:secret@localhost:5432/bookstore?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer pool.Close()

    // 測試連線
    if err := pool.Ping(ctx); err != nil {
        log.Fatal(err)
    }
    fmt.Println("Connected to PostgreSQL!")
}

搭配 database/sql 使用

如果你想保持用 database/sql 的介面(例如搭配 sqlx):

import (
    "database/sql"
    _ "github.com/jackc/pgx/v5/stdlib"
)

db, err := sql.Open("pgx", "postgres://app:secret@localhost:5432/bookstore")

連線池設定

生產環境的連線池設定非常重要:

config, err := pgxpool.ParseConfig("postgres://app:secret@localhost:5432/bookstore")
if err != nil {
    log.Fatal(err)
}

// 連線池參數
config.MaxConns = 25              // 最大連線數
config.MinConns = 5               // 最小維持的空閒連線
config.MaxConnLifetime = time.Hour // 每條連線最長存活時間
config.MaxConnIdleTime = 30 * time.Minute // 空閒連線最長存活時間
config.HealthCheckPeriod = time.Minute    // 健康檢查間隔

pool, err := pgxpool.NewWithConfig(context.Background(), config)

設定建議:

參數 建議值 說明
MaxConns CPU 核心數 * 2 + 磁碟數 經驗法則,通常 20-50
MinConns MaxConns / 5 維持最少的熱連線
MaxConnLifetime 1h 避免長時間持有連線
MaxConnIdleTime 30m 回收長時間沒用的連線

Database Migration

golang-migrate 是 Go 生態中最廣泛使用的 migration 工具。

安裝

# CLI 工具
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

建立 Migration

# 建立 migration 檔案(自動加上時間戳)
migrate create -ext sql -dir migrations -seq create_users

這會產生兩個檔案:

-- migrations/000001_create_users.up.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
-- migrations/000001_create_users.down.sql
DROP TABLE IF EXISTS users;

執行與回滾

# 執行所有 pending migration
migrate -path migrations -database "postgres://app:secret@localhost:5432/bookstore?sslmode=disable" up

# 回滾上一個 migration
migrate -path migrations -database "..." down 1

# 回滾所有
migrate -path migrations -database "..." down

# 查看目前版本
migrate -path migrations -database "..." version

在 Go 程式碼中整合

import (
    "github.com/golang-migrate/migrate/v4"
    _ "github.com/golang-migrate/migrate/v4/database/postgres"
    _ "github.com/golang-migrate/migrate/v4/source/file"
)

func runMigrations(dbURL string) error {
    m, err := migrate.New("file://migrations", dbURL)
    if err != nil {
        return err
    }

    if err := m.Up(); err != nil && err != migrate.ErrNoChange {
        return err
    }
    return nil
}

PostgreSQL 進階功能

Index 策略

-- B-tree(預設):適用於等值和範圍查詢
CREATE INDEX idx_books_title ON books(title);

-- 複合 Index:查詢條件經常一起出現時
CREATE INDEX idx_books_author_year ON books(author, year);

-- 部分 Index:只對符合條件的行建立索引
CREATE INDEX idx_active_users ON users(email) WHERE deleted_at IS NULL;

-- GIN Index:適用於全文搜尋和 JSONB
CREATE INDEX idx_books_metadata ON books USING GIN(metadata);

在 Go 中檢查查詢是否使用 index:

var plan string
err := pool.QueryRow(ctx, "EXPLAIN ANALYZE SELECT * FROM users WHERE email = $1", "[email protected]").Scan(&plan)
fmt.Println(plan) // 看到 "Index Scan" 就表示有用到 index

Transaction Isolation Level

ctx := context.Background()

// 開始一個帶有隔離等級的 transaction
tx, err := pool.BeginTx(ctx, pgx.TxOptions{
    IsoLevel: pgx.ReadCommitted, // 預設
})
// 其他選項:
// pgx.RepeatableRead  — 避免不可重複讀
// pgx.Serializable    — 最嚴格,完全序列化

defer tx.Rollback(ctx)

// 執行操作
_, err = tx.Exec(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", 100, 1)
if err != nil {
    return err
}
_, err = tx.Exec(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", 100, 2)
if err != nil {
    return err
}

return tx.Commit(ctx)

JSONB 欄位

PostgreSQL 的 JSONB 讓你在關聯式資料庫中使用半結構化資料:

-- 建立帶有 JSONB 欄位的表
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    metadata JSONB DEFAULT '{}'
);

-- 插入 JSONB 資料
INSERT INTO products (name, metadata)
VALUES ('Laptop', '{"brand": "Apple", "specs": {"ram": 16, "storage": 512}}');

在 Go 中操作:

import "encoding/json"

type ProductMetadata struct {
    Brand string `json:"brand"`
    Specs struct {
        RAM     int `json:"ram"`
        Storage int `json:"storage"`
    } `json:"specs"`
}

// 寫入
meta := ProductMetadata{Brand: "Apple"}
meta.Specs.RAM = 16
meta.Specs.Storage = 512

metaJSON, _ := json.Marshal(meta)
_, err := pool.Exec(ctx,
    "INSERT INTO products (name, metadata) VALUES ($1, $2)",
    "Laptop", metaJSON,
)

// 讀取
var name string
var metadata ProductMetadata
err := pool.QueryRow(ctx,
    "SELECT name, metadata FROM products WHERE id = $1", 1,
).Scan(&name, &metadata)

// JSONB 查詢(用 PostgreSQL 的 @> 運算子)
rows, err := pool.Query(ctx,
    `SELECT name FROM products WHERE metadata @> '{"brand": "Apple"}'`,
)

// JSONB 取出特定欄位
var brand string
err := pool.QueryRow(ctx,
    "SELECT metadata->>'brand' FROM products WHERE id = $1", 1,
).Scan(&brand)

下一步

資料庫的基礎穩固了,下一篇我們來學 Go 的另一個強項——gRPC,了解如何用 Protocol Buffers 定義服務介面,實現高效能的 RPC 通訊。