Go: Go 数据库操作
Go 的 database/sql 提供了统一的 SQL 数据库操作接口——你只需要换驱动就可以切换 MySQL/SQLite/PostgreSQL。
当你需要安全地操作数据库:防止 SQL 注入、管理事务、配置连接池——database/sql 标准库提供了一套完整且安全的工具。
1. 你将学到
database/sql注册驱动与打开连接db.Query/db.QueryRow/db.Exec三种查询方式db.Prepare预编译防 SQL 注入- transaction:
Begin/Commit/Rollback - 连接池配置:
SetMaxOpenConns/SetMaxIdleConns - SQLite 和 MySQL 驱动使用
2. 一个后端工程师的真实故事
(1) 痛点:字符串拼接 SQL,被黑客注入了删库
Bob 是电商平台的后端工程师,他需要实现一个商品搜索接口:
"我用 fmt.Sprintf 拼接 SQL 查询商品:
SELECT * FROM products WHERE name LIKE '%" + search + "%'。上线第二天,有人在搜索框输入了' OR 1=1; DROP TABLE products;--。我的商品表没了。老板说'数据库里 10 万商品去哪了?'"
// 坏代码:字符串拼接 SQL,致命漏洞
func searchProducts(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("q")
// 如果 search = "' OR 1=1; DROP TABLE products;--"
// 最终 SQL: SELECT * FROM products WHERE name LIKE '%' OR 1=1; DROP TABLE products;--%'
query := fmt.Sprintf("SELECT * FROM products WHERE name LIKE '%%%s%%'", search)
rows, err := db.Query(query) // 噩梦开始
}
(2) Go 的解法:预编译 PreparedStatement
// 好代码:预编译,参数自动转义
func searchProducts(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("q")
// ? 是占位符,数据库驱动自动转义参数
// search 值永远被当作字符串对待,不会成为 SQL 语法的一部分
stmt, err := db.Prepare("SELECT * FROM products WHERE name LIKE ?")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer stmt.Close()
rows, err := stmt.Query("%" + search + "%")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
// 处理结果...
}
(3) 收益:拼接 SQL vs 预编译
| 维度 | 字符串拼接 | 预编译 PreparedStatement |
|---|---|---|
| SQL 注入 | ❌ 高危 | ✅ 自动转义 |
| 性能 | 每次解析 SQL | ✅ 只解析一次,多次执行快 |
| 可读性 | 混乱 | ✅ 清晰 |
| 参数类型 | 全部变字符串 | ✅ 保留类型 |
3. 数据库连接
▶ 示例:连接 SQLite
⚙️ 前置安装:运行
go get github.com/mattn/go-sqlite3⚠️ 注意:go-sqlite3 需要 CGO,Windows 需安装 gcc(MinGW-w64),macOS/Linux 自带
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3" // 导入驱动(_ 表示只执行 init,不直接使用)
)
func main() {
// 打开数据库(实际连接在第一次查询时建立)
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 测试连接是否可达
err = db.Ping()
if err != nil {
log.Fatal("无法连接数据库:", err)
}
fmt.Println("数据库连接成功")
}
▶ 示例:join MySQL
⚙️ 前置安装:运行
go get github.com/go-sql-driver/mysql
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// DSN 格式:user:password@tcp(host:port)/dbname?params
dsn := "root:password@tcp(127.0.0.1:3306)/shop?charset=utf8mb4&parseTime=true"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 连接池配置
db.SetMaxOpenConns(25) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活时间
db.SetConnMaxIdleTime(2 * time.Minute) // 空闲连接最大存活时间
if err = db.Ping(); err != nil {
log.Fatal("无法连接数据库:", err)
}
fmt.Println("MySQL 连接成功")
}
(1) database/sql 关键方法
| 方法 | 用途 | 返回 |
|---|---|---|
sql.Open(driver, dsn) |
打开数据库(懒加载) | *DB, error |
db.Ping() |
测试连接实际是否可达 | error |
db.Close() |
关闭数据库 | error |
db.Exec(sql, args...) |
执行 INSERT/UPDATE/DELETE | Result, error |
db.Query(sql, args...) |
执行 SELECT 返回多行 | *Rows, error |
db.QueryRow(sql, args...) |
执行 SELECT 返回单行 | *Row |
db.Prepare(sql) |
预编译 SQL | *Stmt, error |
sql.Open 不会实际创建连接——它只是验证 DSN 格式。实际连接在第一次 Ping 或 Query / Exec 时建立。 所以 sql.Open 返回 nil error 不代表数据库可达。
4. CRUD 操作
▶ 示例:创建表和插入数据
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./shop.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 创建表
createSQL := `
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
_, err = db.Exec(createSQL)
if err != nil {
log.Fatal(err)
}
fmt.Println("表创建成功")
// 插入数据(Exec 返回 Result)
result, err := db.Exec(
"INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
"Laptop", 999.99, 10,
)
if err != nil {
log.Fatal(err)
}
id, _ := result.LastInsertId()
affected, _ := result.RowsAffected()
fmt.Printf("插入: id=%d, 影响行数=%d\n", id, affected)
}
▶ 示例:查询数据
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
type Product struct {
ID int
Name string
Price float64
Stock int
}
func main() {
db, err := sql.Open("sqlite3", "./shop.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// QueryRow:查询单行
var p Product
err = db.QueryRow("SELECT id, name, price, stock FROM products WHERE id = ?", 1).
Scan(&p.ID, &p.Name, &p.Price, &p.Stock)
if err == sql.ErrNoRows {
fmt.Println("未找到商品")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Printf("商品: %+v\n", p)
}
// Query:查询多行
rows, err := db.Query("SELECT id, name, price, stock FROM products WHERE price < ?", 500)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var pr Product
err := rows.Scan(&pr.ID, &pr.Name, &pr.Price, &pr.Stock)
if err != nil {
log.Fatal(err)
}
fmt.Printf(" 商品: %+v\n", pr)
}
// 检查遍历是否出错
if err = rows.Err(); err != nil {
log.Fatal(err)
}
}
(2) CRUD 方法对比
| 操作 | method | return value | 适用场景 |
|---|---|---|---|
| Create | db.Exec(INSERT...) |
Result (LastInsertId + RowsAffected) |
插入/update/删除 |
| Read 单行 | db.QueryRow(SELECT...).Scan() |
*Row + 自动关闭 |
单行查询 |
| Read 多行 | db.Query(SELECT...); rows.Scan() |
*Rows (需遍历 + 关闭) |
多行查询 |
| Update | db.Exec(UPDATE...) |
Result (RowsAffected) |
更新数据 |
| Delete | db.Exec(DELETE...) |
Result (RowsAffected) |
删除数据 |
rows.Close() 必须调用——即使已经在 for rows.Next() 中遍历完所有行。不关闭会导致连接泄漏(连接不被释放回池)。用 defer rows.Close() 确保释放。
5. 预编译 PreparedStatement
▶ 示例:预编译批量插入
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
type Product struct {
Name string
Price float64
Stock int
}
func main() {
db, err := sql.Open("sqlite3", "./shop.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 预编译 SQL 语句
stmt, err := db.Prepare("INSERT INTO products (name, price, stock) VALUES (?, ?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// 批量插入(SQL 只编译一次)
products := []Product{
{"Mouse", 29.99, 100},
{"Keyboard", 79.99, 50},
{"Monitor", 299.99, 20},
{"USB-C Hub", 49.99, 200},
}
for _, p := range products {
result, err := stmt.Exec(p.Name, p.Price, p.Stock)
if err != nil {
log.Printf("插入失败 %s: %v", p.Name, err)
continue
}
id, _ := result.LastInsertId()
fmt.Printf("插入成功: %s (id=%d)\n", p.Name, id)
}
}
(3) 预编译 vs 拼接 SQL
| 对比项 | 预编译 Prepare |
拼接 SQL |
|---|---|---|
| SQL 注入 | ✅ 自动转义参数 | ❌ 高风险 |
| 性能(多次执行) | ✅ 只解析一次 | ❌ 每次解析 |
| 代码可读性 | 清晰(参数用 ? 占位) | 混乱(引号嵌套) |
| 类型安全 | ✅ 保留参数类型 | ❌ 全转字符串 |
| 适用场景 | 所有用户输入 | 仅静态 SQL(表名/列名,非用户输入) |
6. 事务
▶ 示例:事务转账
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func transferFunds(db *sql.DB, fromID, toID int, amount float64) error {
// 开启事务
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("开启事务失败: %w", err)
}
// 事务失败时回滚
defer tx.Rollback() // 如果 Commit 成功,Rollback 是 no-op
// 1. 从 fromID 扣款
result, err := tx.Exec(
"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
amount, fromID, amount,
)
if err != nil {
return fmt.Errorf("扣款失败: %w", err)
}
affected, _ := result.RowsAffected()
if affected == 0 {
return fmt.Errorf("余额不足或账户不存在")
}
// 2. 向 toID 加款
result, err = tx.Exec(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, toID,
)
if err != nil {
return fmt.Errorf("加款失败: %w", err)
}
affected, _ = result.RowsAffected()
if affected == 0 {
return fmt.Errorf("收款账户不存在")
}
// 提交事务
return tx.Commit()
}
func main() {
db, err := sql.Open("sqlite3", "./bank.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 初始化账户
db.Exec("CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY, balance REAL)")
db.Exec("INSERT OR IGNORE INTO accounts VALUES (1, 1000.00)")
db.Exec("INSERT OR IGNORE INTO accounts VALUES (2, 500.00)")
// 转账 200 从 1 到 2
err = transferFunds(db, 1, 2, 200)
if err != nil {
log.Printf("转账失败: %v\n", err)
} else {
fmt.Println("转账成功!")
}
}
sequenceDiagram
participant App as Application
participant DB as Database
App->>DB: BEGIN
DB-->>App: OK
App->>DB: UPDATE SET balance = balance - ? WHERE id = 1
DB-->>App: 1 row affected
App->>DB: UPDATE SET balance = balance + ? WHERE id = 2
DB-->>App: 1 row affected
App->>DB: COMMIT
DB-->>App: OK (持久化)
Note over App,DB: 如果中途出错 → ROLLBACK<br/>所有变更撤销
defer tx.Rollback() 是安全模式——如果 Commit 成功,Rollback 调用是安全的(no-op)。如果 Commit 失败,defer 会自动回滚。永远不要省略 Rollback!
7. 连接池配置
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
func configurePool(db *sql.DB) {
// 最大打开连接数(达到此数后,新请求排队等待)
db.SetMaxOpenConns(25)
// 最大空闲连接数(保持打开但未使用的连接)
db.SetMaxIdleConns(10)
// 连接最大存活时间(防止长时间运行的连接被数据库断开)
db.SetConnMaxLifetime(5 * time.Minute)
// 空闲连接最大存活时间
db.SetConnMaxIdleTime(2 * time.Minute)
}
func main() {
db, _ := sql.Open("mysql", "user:pass@/dbname")
configurePool(db)
fmt.Println("连接池配置完成")
}
(4) 连接池参数
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
SetMaxOpenConns |
0(无限制) | 25-100 | 最大并发连接数 |
SetMaxIdleConns |
2 | 10-25 | 最大空闲连接数(<= MaxOpenConns) |
SetConnMaxLifetime |
0(永不过期) | 5-30min | 连接最大存活时间 |
SetConnMaxIdleTime |
0(永不过期) | 2-5min | 空闲连接超时 |
MaxIdleConns 不能大于 MaxOpenConns——database/sql 会自动调整。另外,不要设置 MaxOpenConns=0(无限制)——高并发下会创建大量连接拖垮数据库。始终设置一个合理的上限。
8. 完整示例:电商库存管理
// inventory.go
package main
import (
"database/sql"
"fmt"
"log"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
)
// ---------- Store ----------
type InventoryStore struct {
db *sql.DB
mu sync.RWMutex
}
func NewInventoryStore(dbPath string) (*InventoryStore, error) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("打开数据库失败: %w", err)
}
// 连接池配置
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
store := &InventoryStore{db: db}
if err := store.initSchema(); err != nil {
return nil, fmt.Errorf("初始化表失败: %w", err)
}
return store, nil
}
func (s *InventoryStore) initSchema() error {
schema := `
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
total REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id)
);`
_, err := s.db.Exec(schema)
return err
}
// ---------- Product Operations ----------
func (s *InventoryStore) AddProduct(name string, price float64, stock int) (int64, error) {
result, err := s.db.Exec(
"INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
name, price, stock,
)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func (s *InventoryStore) GetProduct(id int) (product, error) {
var p product
err := s.db.QueryRow(
"SELECT id, name, price, stock, created_at FROM products WHERE id = ?", id,
).Scan(&p.ID, &p.Name, &p.Price, &p.Stock, &p.CreatedAt)
if err == sql.ErrNoRows {
return p, fmt.Errorf("商品不存在")
}
return p, err
}
type product struct {
ID int
Name string
Price float64
Stock int
CreatedAt time.Time
}
// ---------- Order Operations(transaction) ----------
type OrderRequest struct {
ProductID int
Quantity int
}
func (s *InventoryStore) PlaceOrder(req OrderRequest) (int64, error) {
s.mu.Lock() // 防止超卖:同一时间只有一个订单在扣减
defer s.mu.Unlock()
tx, err := s.db.Begin()
if err != nil {
return 0, fmt.Errorf("开启事务失败: %w", err)
}
defer tx.Rollback() // 安全回滚
// 1. 查询商品并锁定行(⚠️ SQLite 不支持 FOR UPDATE,此处用 MySQL/PostgreSQL)
var price float64
var stock int
err = tx.QueryRow(
"SELECT price, stock FROM products WHERE id = ? FOR UPDATE",
req.ProductID,
).Scan(&price, &stock)
if err == sql.ErrNoRows {
return 0, fmt.Errorf("商品不存在")
}
if err != nil {
return 0, err
}
// 2. 检查库存
if stock < req.Quantity {
return 0, fmt.Errorf("库存不足: 需要 %d, 剩余 %d", req.Quantity, stock)
}
// 3. 扣减库存
result, err := tx.Exec(
"UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?",
req.Quantity, req.ProductID, req.Quantity,
)
if err != nil {
return 0, err
}
affected, _ := result.RowsAffected()
if affected == 0 {
return 0, fmt.Errorf("并发库存不足")
}
// 4. 创建订单
total := price * float64(req.Quantity)
result, err = tx.Exec(
"INSERT INTO orders (product_id, quantity, total) VALUES (?, ?, ?)",
req.ProductID, req.Quantity, total,
)
if err != nil {
return 0, err
}
orderID, _ := result.LastInsertId()
// 5. commit
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("提交事务失败: %w", err)
}
return orderID, nil
}
// ---------- Report ----------
func (s *InventoryStore) LowStockReport(threshold int) ([]product, error) {
rows, err := s.db.Query(
"SELECT id, name, price, stock, created_at FROM products WHERE stock < ? ORDER BY stock ASC",
threshold,
)
if err != nil {
return nil, 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.CreatedAt); err != nil {
return nil, err
}
products = append(products, p)
}
return products, rows.Err()
}
// ---------- Main ----------
func main() {
store, err := NewInventoryStore("./inventory.db")
if err != nil {
log.Fatal(err)
}
// 添加商品
laptopID, _ := store.AddProduct("Laptop", 999.99, 10)
mouseID, _ := store.AddProduct("Mouse", 29.99, 100)
fmt.Printf("添加商品: laptop=%d, mouse=%d\n", laptopID, mouseID)
// 下单(事务安全)
orderID, err := store.PlaceOrder(OrderRequest{ProductID: int(laptopID), Quantity: 2})
if err != nil {
log.Printf("下单失败: %v\n", err)
} else {
fmt.Printf("下单成功: order=%d\n", orderID)
}
// 低库存报告
lowStock, _ := store.LowStockReport(20)
fmt.Printf("低库存商品 (%d 个):\n", len(lowStock))
for _, p := range lowStock {
fmt.Printf(" %s: stock=%d\n", p.Name, p.Stock)
}
}
FOR UPDATE 行锁——该子句会被静默忽略。上面的代码在 MySQL/PostgreSQL 中才能真正锁行防并发超卖;在 SQLite 中靠 mu.Lock() 互斥锁保证安全。如需跨数据库兼容,建议用 UPDATE ... WHERE stock >= ? 的条件更新替代行锁。
❓ 常见问题
' OR 1=1 只是字符串,不会变成 WHERE 条件。tx.Rollback() 在出错时撤销所有变更。defer tx.Rollback() + tx.Commit() 是标准模式——Commit 成功则 Rollback 是 no-op,Commit 失败则自动回滚。这确保了原子性。?,MySQL 用 ?,PostgreSQL 用 $1;(2) SQL 语法差异(如自增主键写法);(3) 并发性能 SQLite 不如 MySQL。rows.Close() 释放连接回池。使用 defer rows.Close() 在 rows 创建后立即注册,确保任何路径都释放资源。db.SetMaxOpenConns 和 db.SetMaxIdleConns 确保连接池不成为瓶颈;(2) 用数据库自身慢查询日志;(3) 用 EXPLAIN ANALYZE 分析查询计划;(4) 考虑添加 SQL 日志中间件记录所有查询耗时。📖 小节
sql.Open打开连接(懒加载)+db.Ping验证连通性db.Exec/db.Query/db.QueryRow三种查询Prepare预编译防 SQL 注入- 事务:
Begin→Commit/Rollback defer tx.Rollback()安全模式- 连接池:
SetMaxOpenConns/SetMaxIdleConns/SetConnMaxLifetime - SQLite 用
?,MySQL 用?,PostgreSQL 用$1
📝 作业
-
基础题(难度⭐):创建 SQLite 数据库,建一个
tasks表(id, title, done, created_at)。实现 InsertTask、ListTasks(按日期排序)、MarkDone(按 id 更新)三个函数。 -
进阶题(难度⭐⭐):实现一个博客系统的数据库层。要求:(1) posts 表(id, title, content, author_id, created_at);(2) 支持分页查询(LIMIT/OFFSET);(3) 预编译插入防注入;(4) 事务:发布文章时同时更新作者的文章数量;(5) 使用
-race验证并发安全。 -
挑战题(难度⭐⭐⭐):实现一个库存管理系统的并发安全操作。要求:(1) 100 个 goroutine 同时下单(每个扣减库存不同商品);(2) 使用 database/sql 事务 + 应用层 Mutex 防止超卖;(3) 预编译所有 SQL;(4) 连接池配置合理;(5) 统计成功/失败的订单数和最终库存。