跳到主要内容

database/sql

问题

Go 标准库的 database/sql 怎么用?如何管理连接池和事务?

答案

基本使用

import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // 注册 MySQL 驱动
)

// 打开连接(不会立即建立连接)
db, err := sql.Open("mysql", "user:pass@tcp(localhost:3306)/dbname?parseTime=true")
if err != nil {
log.Fatal(err)
}
defer db.Close()

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

// 连接池配置
db.SetMaxOpenConns(100)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)

CRUD 操作

// 查询单行
var user User
err := db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = ?", userID,
).Scan(&user.ID, &user.Name, &user.Email)

if err == sql.ErrNoRows {
// 未找到
} else if err != nil {
// 其他错误
}

// 查询多行
rows, err := db.QueryContext(ctx,
"SELECT id, name FROM users WHERE age > ?", 18,
)
if err != nil { return err }
defer rows.Close() // 必须 Close!

var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return err
}
users = append(users, u)
}
// 检查迭代错误
if err := rows.Err(); err != nil {
return err
}

// 插入
result, err := db.ExecContext(ctx,
"INSERT INTO users (name, email) VALUES (?, ?)", name, email,
)
id, _ := result.LastInsertId()
affected, _ := result.RowsAffected()

// 更新
result, err := db.ExecContext(ctx,
"UPDATE users SET name = ? WHERE id = ?", name, id,
)
SQL 注入防御
// ❌ 字符串拼接——SQL 注入风险!
query := "SELECT * FROM users WHERE name = '" + name + "'"

// ✅ 参数化查询——永远用 ?(MySQL)或 $1(PostgreSQL)
db.QueryContext(ctx, "SELECT * FROM users WHERE name = ?", name)

事务

tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
// defer + 错误处理确保事务不会挂起
defer tx.Rollback() // 如果已 Commit 则 Rollback 是 no-op

_, 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()

预编译语句

stmt, err := db.PrepareContext(ctx, "SELECT * FROM users WHERE id = ?")
if err != nil { return err }
defer stmt.Close()

// 多次执行同一语句
for _, id := range userIDs {
var user User
stmt.QueryRowContext(ctx, id).Scan(&user.ID, &user.Name)
}

常见面试问题

Q1: rows.Close() 不调用会怎样?

答案:数据库连接不会归还连接池,导致连接泄漏。连接池中的可用连接逐渐减少,最终所有请求都在等待连接。

Q2: database/sql 和 ORM 怎么选?

答案

  • 性能敏感、SQL 简单 → database/sqlsqlx
  • 快速开发、模型复杂 → GORM
  • 类型安全、复杂查询 → sqlc(SQL 生成 Go 代码)

相关链接