Skip to content
Go
原生 SQL 操作与 GORM 对比

原生 SQL 操作与 GORM 对比

本章深入讲解 Go 标准库 database/sql 的完整用法,并与 GORM 进行全面对比,帮助你根据场景做出正确的技术选择。


目录


10.1 database/sql 核心概念

database/sql 是 Go 标准库提供的数据库操作抽象层。它不直接连接数据库,而是通过驱动(Driver)来与不同的数据库交互。

你的代码
    ↓
database/sql(标准库:连接池、事务、查询抽象)
    ↓
database/sql/driver(驱动接口)
    ↓
go-sql-driver/mysql / lib/pq / pgx …(具体驱动实现)
    ↓
MySQL / PostgreSQL 数据库

database/sql 提供什么:

能力 说明
sql.DB 连接池管理器(非单个连接)
Query / QueryRow 查询操作
Exec 写入操作(INSERT/UPDATE/DELETE)
Prepare 预编译语句
Begin / Commit / Rollback 事务管理
Scan 将结果写入 Go 变量
Null* 类型 处理可空字段

database/sql 不提供什么:

能力 替代方案
SQL 构建器 手写 SQL 或 sqlx/squirrel
ORM 映射 GORM / sqlx 的 StructScan
迁移 golang-migrate / Atlas
关联加载 手动 Join 或多次查询
钩子/回调 手动封装

10.2 连接数据库

安装驱动

# MySQL 驱动
go get github.com/go-sql-driver/mysql

# PostgreSQL 驱动(推荐 pgx,也可用 lib/pq)
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/stdlib  # database/sql 适配器

# 或
go get github.com/lib/pq

MySQL 连接

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql" // 匿名导入,注册驱动
)

func main() {
    dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 验证连接
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    // 连接池配置
    db.SetMaxOpenConns(100)
    db.SetMaxIdleConns(10)
    db.SetConnMaxLifetime(30 * time.Minute)
    db.SetConnMaxIdleTime(5 * time.Minute)
}

🚨 陷阱sql.Open 不会立即建立连接——它只是验证 DSN 格式和初始化连接池。使用 db.Ping() 才能确认数据库确实可达。

PostgreSQL 连接(pgx)

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

func main() {
    dsn := "postgres://user:pass@localhost:5432/dbname?sslmode=disable"
    db, err := sql.Open("pgx", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }
}

10.3 查询操作

单行查询 — QueryRow

type User struct {
    ID        int
    Name      string
    Age       int
    CreatedAt time.Time
}

var user User
row := db.QueryRow("SELECT id, name, age, created_at FROM users WHERE id = ?", 1)

err := row.Scan(&user.ID, &user.Name, &user.Age, &user.CreatedAt)
if err == sql.ErrNoRows {
    fmt.Println("用户不存在")
} else if err != nil {
    log.Fatal(err)
}

🚨 陷阱QueryRowScan 才会执行查询。QueryRow 本身不返回错误——必须调用 Scan 并检查其返回的 error。此外,QueryRow 返回的 *Row 不会自动关闭结果集;如果未调用 Scan,后续读取可能阻塞。

多行查询 — Query

rows, err := db.Query("SELECT id, name, age FROM users WHERE age > ?", 18)
if err != nil {
    log.Fatal(err)
}
defer rows.Close() // ← 必须关闭,否则连接泄漏

var users []User
for rows.Next() {
    var u User
    if err := rows.Scan(&u.ID, &u.Name, &u.Age); err != nil {
        log.Fatal(err)
    }
    users = append(users, u)
}

// 检查遍历过程中是否有错误
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

🔬 深入原理rows.Next() 逐行读取结果集。必须在循环后调用 rows.Err() 检查中途发生的错误。rows.Close() 必须用 defer 或显式调用——不关闭会导致连接泄漏。

查询到自定义结构体(手动映射)

// database/sql 不支持直接扫描到结构体,需逐字段映射
rows, _ := db.Query("SELECT u.id, u.name, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id")
defer rows.Close()

type UserStats struct {
    ID         int
    Name       string
    OrderCount int
}
var stats []UserStats
for rows.Next() {
    var s UserStats
    rows.Scan(&s.ID, &s.Name, &s.OrderCount)
    stats = append(stats, s)
}

10.4 写入操作

Exec — 执行 INSERT/UPDATE/DELETE

// INSERT
result, err := db.Exec("INSERT INTO users (name, age) VALUES (?, ?)", "张三", 18)
if err != nil {
    log.Fatal(err)
}
id, _ := result.LastInsertId()  // 自增 ID(MySQL 支持,PostgreSQL 不支持)
n, _ := result.RowsAffected()   // 影响行数

// UPDATE
result, err = db.Exec("UPDATE users SET age = ? WHERE id = ?", 20, 1)
n, _ = result.RowsAffected()

// DELETE
result, err = db.Exec("DELETE FROM users WHERE id = ?", 1)

// PostgreSQL — 用 RETURNING 获取插入 ID
var newID int
err = db.QueryRow(
    "INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
    "张三", 18,
).Scan(&newID)

🚨 陷阱result.LastInsertId() 在 PostgreSQL 中不工作(pgx 驱动返回 -1 并报错)。PG 中要用 RETURNING id 语法。

批量插入

// 手动拼接参数(数据库限制了参数数量)
func BatchInsert(db *sql.DB, users []User) error {
    if len(users) == 0 {
        return nil
    }

    // 构建批量 INSERT
    query := "INSERT INTO users (name, age) VALUES "
    args := make([]interface{}, 0, len(users)*2)

    for i, u := range users {
        if i > 0 {
            query += ", "
        }
        query += "(?, ?)"
        args = append(args, u.Name, u.Age)
    }

    _, err := db.Exec(query, args...)
    return err
}

⚡ 对于大量数据(> 1000 条),建议分批插入并限制每批数量,避免超出 max_allowed_packet


10.5 事务

// 开启事务
tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}

// 确保事务结束(Commit 或 Rollback)
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
        panic(r)
    }
}()

// 操作 1
result, err := tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
if err != nil {
    tx.Rollback()
    return
}
if n, _ := result.RowsAffected(); n == 0 {
    tx.Rollback()
    return
}

// 操作 2
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
if err != nil {
    tx.Rollback()
    return
}

// 提交
err = tx.Commit()
if err != nil {
    log.Fatal(err)
}

事务 + Context

func TransferWithTimeout(ctx context.Context, db *sql.DB, fromID, toID int, amount float64) error {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    tx, err := db.BeginTx(ctx, &sql.TxOptions{
        Isolation: sql.LevelReadCommitted,
        ReadOnly:  false,
    })
    if err != nil {
        return err
    }
    defer tx.Rollback() // Commit 后 Rollback 是空操作

    _, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
    if err != nil {
        return err
    }

    _, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
    if err != nil {
        return err
    }

    return tx.Commit()
}

💡 defer tx.Rollback() 是一个惯用模式——如果 Commit 成功,后续的 Rollback 是空操作;如果函数提前返回,Rollback 确保事务被回滚。


10.6 Prepared Statement

预编译语句:将 SQL 模板发送到数据库预编译,后续调用只需传参数,减少编译开销:

// 预编译
stmt, err := db.Prepare("SELECT id, name, age FROM users WHERE age > ?")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

// 多次执行
rows1, _ := stmt.Query(18)
rows2, _ := stmt.Query(30)

// 预编译写入
insertStmt, err := db.Prepare("INSERT INTO users (name, age) VALUES (?, ?)")
defer insertStmt.Close()

for i := 0; i < 1000; i++ {
    insertStmt.Exec(fmt.Sprintf("user_%d", i), i%50)
}
特性 database/sql Prepare GORM PrepareStmt
自主管理 ✅ 手动创建/销毁 ✅ 自动缓存
性能 高(复用预编译) 高(自动缓存)
灵活性 完全控制 透明

性能提示db.Prepare 在 MySQL 中创建服务端预编译语句,适合批量操作。但如果 SQL 只执行一次,Prepare + Exec 比直接 Exec 多一次网络往返,反而更慢。


10.7 NULL 值处理

database/sql 中,如果查询的列为 NULL 而你用 Go 基础类型(如 stringint)接收,会报错。

使用 Null 类型

import "database/sql"

type User struct {
    ID      int
    Name    string
    Email   sql.NullString  // 可能为 NULL
    Age     sql.NullInt64   // 可能为 NULL
    Bio     sql.NullString  // 可能为 NULL
}

var u User
row := db.QueryRow("SELECT id, name, email, age, bio FROM users WHERE id = ?", 1)
row.Scan(&u.ID, &u.Name, &u.Email, &u.Age, &u.Bio)

// 检查 NULL
if u.Email.Valid {
    fmt.Println(u.Email.String)
} else {
    fmt.Println("邮箱未设置")
}

使用指针

// 指针方案:Go 中 nil 表示 NULL
type User struct {
    ID    int
    Name  string
    Email *string // nil → NULL
    Age   *int    // nil → NULL
}

COALESCE / IFNULL

// 在 SQL 中用默认值替代 NULL
row := db.QueryRow("SELECT id, name, COALESCE(email, '') as email, COALESCE(age, 0) as age FROM users WHERE id = ?", 1)
var u User // 用基础类型即可
row.Scan(&u.ID, &u.Name, &u.Email, &u.Age)
方案 优点 缺点
sql.NullXxx 语义明确,区分"未设置"和"零值" 代码冗长,无 JSON 序列化支持
指针 *T 直观,与 JSON 协同好 内存分配,判断需 != nil
COALESCE 代码最简洁 无法区分"DB 中是 NULL"和"真的是默认值"

💡 sql.NullString/sql.NullInt64 不支持 JSON 序列化。若同时需要 JSON 输出,用指针 *string/*int,或使用 github.com/guregu/null 等第三方库。


10.8 连接池管理

sql.DB 是连接池,而非单连接——创建一次,全局复用:

// ❌ 错误:每次打开新连接池
func handler(w http.ResponseWriter, r *http.Request) {
    db, _ := sql.Open("mysql", dsn)
    db.Query("SELECT ...") // 每次请求创建新连接池!泄漏!
}

// ✅ 正确:全局单例
var DB *sql.DB

func init() {
    DB, _ = sql.Open("mysql", dsn)
    DB.SetMaxOpenConns(100)
    DB.SetMaxIdleConns(10)
    DB.SetConnMaxLifetime(time.Hour)
}

func handler(w http.ResponseWriter, r *http.Request) {
    DB.QueryContext(r.Context(), "SELECT ...")
}

连接池监控

stats := db.Stats()
fmt.Printf(`Database Pool Stats:
  当前打开连接:  %d
  空闲连接:      %d
  使用中连接:    %d
  累计等待次数:  %d (高值表示池太小)
  等待时长合计:  %v
  因空闲关闭:    %d
  因超时关闭:    %d
`,
    stats.OpenConnections,
    stats.Idle,
    stats.InUse,
    stats.WaitCount,
    stats.WaitDuration,
    stats.MaxIdleClosed,
    stats.MaxLifetimeClosed,
)

WaitCount > 0 表示连接池不够用——需增加 MaxOpenConns 或优化查询速度。


10.9 错误处理

检查特定错误

import (
    "database/sql"
    "errors"
    "github.com/go-sql-driver/mysql" // MySQL 特定错误
    "github.com/jackc/pgx/v5/pgconn" // PostgreSQL 特定错误
)

// 通用
if errors.Is(err, sql.ErrNoRows) {
    // 无结果
}

// MySQL 特定错误
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) {
    switch mysqlErr.Number {
    case 1062:
        fmt.Println("主键/唯一键重复")
    case 1451:
        fmt.Println("外键约束冲突")
    case 1213:
        fmt.Println("死锁,需要重试")
    }
}

// PostgreSQL 特定错误
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
    switch pgErr.Code {
    case "23505":
        fmt.Println("唯一约束冲突")
    case "23503":
        fmt.Println("外键约束冲突")
    case "40P01":
        fmt.Println("死锁")
    }
}

死锁重试

func ExecWithRetry(db *sql.DB, fn func(tx *sql.Tx) error, maxRetries int) error {
    for i := 0; i < maxRetries; i++ {
        tx, _ := db.Begin()
        err := fn(tx)
        if err == nil {
            return tx.Commit()
        }
        tx.Rollback()

        if !isDeadlockError(err) {
            return err // 非死锁错误直接返回
        }
        time.Sleep(time.Millisecond * time.Duration(10*(i+1))) // 退避策略
    }
    return errors.New("达到最大重试次数")
}

func isDeadlockError(err error) bool {
    var mysqlErr *mysql.MySQLError
    if errors.As(err, &mysqlErr) && mysqlErr.Number == 1213 {
        return true
    }
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) && pgErr.Code == "40P01" {
        return true
    }
    return false
}

10.10 sqlx — 增强版 database/sql

sqlxdatabase/sql 的轻量级扩展,提供结构体映射和命名参数:

go get github.com/jmoiron/sqlx

核心优势

import "github.com/jmoiron/sqlx"
import _ "github.com/go-sql-driver/mysql"

// sqlx.DB 内嵌了 sql.DB,所有 database/sql 方法都能用
db, _ := sqlx.Connect("mysql", dsn)

type User struct {
    ID        int       `db:"id"`
    Name      string    `db:"name"`
    Age       int       `db:"age"`
    CreatedAt time.Time `db:"created_at"`
}

// StructScan — 自动映射到结构体
var user User
db.Get(&user, "SELECT id, name, age, created_at FROM users WHERE id = ?", 1)

// 多行映射
var users []User
db.Select(&users, "SELECT id, name, age FROM users WHERE age > ?", 18)

// 命名参数
sql := "SELECT * FROM users WHERE name = :name AND age > :age"
arg := map[string]interface{}{"name": "张三", "age": 18}
rows, _ := db.NamedQuery(sql, arg)

// IN 查询(自动展开切片)
ids := []int{1, 2, 3}
query, args, _ := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
db.Select(&users, query, args...) // sqlx.In 会生成 3 个 ? 占位符

// 事务
tx := db.MustBegin()
tx.Exec("INSERT INTO users (name) VALUES (?)", "张三")
tx.Commit()
特性 database/sql sqlx GORM
结构体映射 ❌ 手动 Scan db tag gorm tag
命名参数 NamedQuery sql.Named
IN 展开 ❌ 手动拼接 sqlx.In ✅ 自动
SQL 构建器 ✅ 链式调用
关联加载 ✅ Preload
迁移 ✅ AutoMigrate
学习成本
性能开销 最低(基准) 极低(thin wrapper) 中等(ORM 抽象层)

10.11 go-sqlbuilder / squirrel — SQL 构建器

当你不想用 ORM 但又需要动态构建 SQL 时,SQL 构建器是很好的中间选择:

squirrel

import sq "github.com/Masterminds/squirrel"

// MySQL 风格
users := sq.Select("*").From("users").Where(sq.Eq{"age": 18}).OrderBy("name DESC").Limit(10)
sql, args, _ := users.ToSql()
// SELECT * FROM users WHERE age = ? ORDER BY name DESC LIMIT 10

// PostgreSQL 风格
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
sql, args, _ := psql.Select("*").From("users").Where(sq.Eq{"age": 18}).ToSql()
// SELECT * FROM users WHERE age = $1

go-sqlbuilder

import "github.com/huandu/go-sqlbuilder"

sql, args := sqlbuilder.NewSelectBuilder().
    Select("id", "name").
    From("users").
    Where(
        sqlbuilder.GreaterThan("age", 18),
        sqlbuilder.Equal("status", "active"),
    ).
    OrderBy("created_at DESC").
    Limit(10).
    Build()

10.12 GORM vs database/sql 全面对比

对比总表

维度 database/sql GORM
定位 标准库数据库抽象层 全功能 ORM
学习曲线 低(标准库 API) 中(需学习框架约定和 Tag 系统)
代码量 较多(手动 Scan、拼接 SQL) 较少(链式调用、自动映射)
SQL 控制 完全控制(你写什么就是什么) 高度自动(需理解 Query Builder 生成逻辑)
性能 最高(零抽象开销) 略低(反射、Callback、关联处理)
结构体映射 手动 Scan 自动(反射 + Tag)
关联加载 手动 Join + 组装 Preload / Joins + 自动填充
迁移 AutoMigrate(开发环境)
钩子/回调 完整生命周期钩子
批量操作 手动拼接 SQL CreateInBatches / 链式 Updates
事务 Begin/Commit/Rollback 闭包事务 / 手动事务 / SavePoint
SQL 日志 内置 Logger(含慢查询标记)
动态 SQL 手动拼接(需防护注入) Scope / 链式条件组合
依赖 仅标准库 + 驱动 gorm.io/gorm + 驱动
生成 SQL 质量 取决于你写的 SQL 大多数情况好,复杂场景需检查
适合项目 性能敏感、SQL 密集型 常规 CRUD 为主、快速开发

性能对比 (benchmark 参考)

操作 database/sql GORM 差异
插入 1 行 ~150 μs ~250 μs GORM 慢 ~40%
插入 1000 行 (batch) ~3 ms ~5 ms GORM 慢 ~40%
查询 100 行 (struct scan) ~2 ms ~4 ms GORM 慢 ~50%
复杂 JOIN 查询 ~5 ms ~8 ms GORM 慢 ~37%

注:以上为大致参考值,实际性能受驱动、数据库版本、网络延迟等影响。GORM 的性能开销主要来自反射(结构体映射)、回调链(生命周期钩子)、和 Preload 的多条 SQL。

代码量对比

database/sql 方式:

// 查询单条 ≈ 12 行
row := db.QueryRow("SELECT id, name, age, email, phone, created_at FROM users WHERE id = ?", id)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Age, &u.Email, &u.Phone, &u.CreatedAt)
if err != nil { ... }

// 查询全部 ≈ 15 行
rows, _ := db.Query("SELECT id, name, age FROM users WHERE age > ?", 18)
defer rows.Close()
var users []User
for rows.Next() {
    var u User
    rows.Scan(&u.ID, &u.Name, &u.Age)
    users = append(users, u)
}
rows.Err()

// 更新 ≈ 5 行
db.Exec("UPDATE users SET name = ?, age = ? WHERE id = ?", name, age, id)

// 插入 ≈ 5 行
result, _ := db.Exec("INSERT INTO users (name, age) VALUES (?, ?)", name, age)
id, _ := result.LastInsertId()

// 事务 ≈ 18 行
tx, _ := db.Begin()
defer tx.Rollback()
tx.Exec(...)
tx.Exec(...)
tx.Commit()

GORM 方式:

// 查询单条 ≈ 2 行
db.First(&user, id)

// 查询全部 ≈ 2 行
db.Where("age > ?", 18).Find(&users)

// 更新 ≈ 2 行
db.Model(&user).Updates(User{Name: name, Age: age})

// 插入 ≈ 1 行
db.Create(&user) // 自动回填 ID

// 事务 ≈ 5 行
db.Transaction(func(tx *gorm.DB) error {
    tx.Create(...)
    tx.Create(...)
    return nil
})

10.13 如何选择与混合使用

选择决策树

你需要什么?
├── 简单 CRUD 为主(80% 以上是基础操作)
│   └── 用 GORM —— 省时省力,代码简洁
│
├── 复杂 SQL 为主(报表、BI、数据分析)
│   ├── 用 database/sql —— 完全控制 SQL
│   └── 或用 sqlx —— 保留 SQL 控制权 + 结构体映射
│
├── 性能敏感(高并发、低延迟)
│   ├── database/sql + 手写 SQL —— 零抽象开销
│   └── 或用 GORM + Raw SQL(性能热点处)
│
├── 团队 SQL 能力较弱
│   └── 用 GORM —— 减少手写 SQL 出错概率
│
├── 需要关联操作(Preload、多级嵌套)
│   └── 用 GORM —— 手写嵌套关联极繁琐
│
└── 需要灵活性与可控性兼顾
    ├── sqlx —— 轻量结构体映射
    └── 或 GORM + Raw SQL 混合 —— 80% 操作用 GORM,20% 复杂操作用 Raw

混合使用方案(推荐)

在实际项目中,最好的方式往往是混合使用 GORM 和原生 SQL:

// 1. 全局使用 GORM
var DB *gorm.DB

// 2. 简单 CRUD:用 GORM
func GetUser(id uint) (*User, error) {
    var user User
    err := DB.First(&user, id).Error
    return &user, err
}

func ListActiveUsers(page, size int) ([]User, int64, error) {
    var users []User
    var total int64
    DB.Model(&User{}).Where("status = ?", "active").Count(&total)
    DB.Where("status = ?", "active").Scopes(Paginate(page, size)).Find(&users)
    return users, total, nil
}

// 3. 复杂统计查询:用 Raw SQL
type SalesReport struct {
    Date   string  `json:"date"`
    Total  float64 `json:"total"`
    Orders int     `json:"orders"`
}

func GetSalesReport(start, end time.Time) ([]SalesReport, error) {
    var reports []SalesReport
    err := DB.Raw(`
        SELECT
            DATE(created_at) as date,
            SUM(amount) as total,
            COUNT(*) as orders
        FROM orders
        WHERE created_at BETWEEN ? AND ?
        GROUP BY DATE(created_at)
        ORDER BY date
    `, start, end).Scan(&reports).Error
    return reports, err
}

// 4. 高性能写入:使用 Exec(跳过 GORM 的反射和回调)
func BatchUpdateStatus(userIDs []uint, status string) error {
    return DB.Exec(
        "UPDATE users SET status = ? WHERE id IN (?)",
        status, userIDs,
    ).Error
}

// 5. 复杂事务 + 悲观锁:混合使用
func TransferMoney(fromID, toID uint, amount float64) error {
    return DB.Transaction(func(tx *gorm.DB) error {
        // 用 Raw SQL 做行锁(GORM 的 Clauses(Locking) 也可)
        var balance float64
        row := tx.Raw(
            "SELECT balance FROM accounts WHERE id = ? FOR UPDATE",
            fromID,
        ).Row()
        row.Scan(&balance)

        if balance < amount {
            return errors.New("余额不足")
        }

        tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
        tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
        return nil
    })
}

在 GORM 中使用 sqlx

// GORM 提供 *sql.DB 访问
sqlDB, _ := DB.DB()

// 直接用 sqlx 封装(共享连接池)
sqlxDB := sqlx.NewDb(sqlDB, "mysql")

// 复杂查询用 sqlx 的结构体映射
type ComplexResult struct {
    UserName  string `db:"user_name"`
    OrderCnt  int    `db:"order_cnt"`
    TotalAmt  float64 `db:"total_amt"`
}
var results []ComplexResult
sqlxDB.Select(&results, `
    SELECT u.name as user_name, COUNT(o.id) as order_cnt, SUM(o.amount) as total_amt
    FROM users u
    LEFT JOIN orders o ON o.user_id = u.id
    WHERE u.created_at > ?
    GROUP BY u.id
    HAVING order_cnt > 5
`, startDate)

总结

场景 推荐方案
快速原型 / 小中型项目 GORM — 全功能、低代码量
高性能 / 低延迟系统 database/sql — 零开销,完全控制
CRUD 为主,部分复杂查询 GORM + Raw SQL 混合 — 两者优势兼得
SQL 密集 / 报表 / 数据分析 database/sqlsqlx
团队 ORM 经验丰富 GORM
团队 SQL 经验丰富 database/sqlsqlx
需要迁移 / 钩子 / 关联 GORM
微服务(表少、关系简单) sqlxdatabase/sql — 够用且轻量

💡 核心原则:没有银弹。用最能表达意图的工具——简单的事用 GORM 节省时间,复杂的事用原生 SQL 确保精确控制。在同一个项目中混合使用两者是完全可行的,也是很多生产项目的实际做法。