Go: Go 数据库操作

Go 的 database/sql 提供了统一的 SQL 数据库操作接口——你只需要换驱动就可以切换 MySQL/SQLite/PostgreSQL。

当你需要安全地操作数据库:防止 SQL 注入、管理事务、配置连接池——database/sql 标准库提供了一套完整且安全的工具。

1. 你将学到


2. 一个后端工程师的真实故事

(1) 痛点:字符串拼接 SQL,被黑客注入了删库

Bob 是电商平台的后端工程师,他需要实现一个商品搜索接口:

"我用 fmt.Sprintf 拼接 SQL 查询商品:SELECT * FROM products WHERE name LIKE '%" + search + "%'。上线第二天,有人在搜索框输入了 ' OR 1=1; DROP TABLE products;--。我的商品表没了。老板说'数据库里 10 万商品去哪了?'"

GO
// 坏代码:字符串拼接 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

GO
// 好代码:预编译,参数自动转义
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 自带

GO
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

GO
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 格式。实际连接在第一次 PingQuery / Exec 时建立。 所以 sql.Open 返回 nil error 不代表数据库可达。


4. CRUD 操作

▶ 示例:创建表和插入数据

GO
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)
}
▶ 试一试

▶ 示例:查询数据

GO 📖 仅展示
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)
    }
}
逻辑代码 46 行(超过 40 行限制,仅展示)

(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

▶ 示例:预编译批量插入

GO
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. 事务

▶ 示例:事务转账

GO 📖 仅展示
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("转账成功!")
    }
}
逻辑代码 53 行(超过 40 行限制,仅展示)
100%
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. 连接池配置

GO
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. 完整示例:电商库存管理

GO
// 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)
    }
}
⚠️ 注意: SQLite 不支持 FOR UPDATE 行锁——该子句会被静默忽略。上面的代码在 MySQL/PostgreSQL 中才能真正锁行防并发超卖;在 SQLite 中靠 mu.Lock() 互斥锁保证安全。如需跨数据库兼容,建议用 UPDATE ... WHERE stock >= ? 的条件更新替代行锁。


❓ 常见问题

Q database/sql 和 ORM(如 GORM)怎么选?
A 简单 CRUD 用 GORM 更快。需要精细控制 SQL、高并发、复杂查询时用 database/sql。建议:小型项目用 GORM,大型项目用 database/sql + 查询构建器(如 squirrel)。
Q 预编译为什么能防 SQL 注入?
A 预编译把 SQL 结构和参数分开传输。数据库先编译 SQL 模板(确定语法结构),再把参数作为纯数据绑定。参数永远不被当作 SQL 语法解析——所以 ' OR 1=1 只是字符串,不会变成 WHERE 条件。
Q 事务 ACID 怎么在 Go 中保证?
Atx.Rollback() 在出错时撤销所有变更。defer tx.Rollback() + tx.Commit() 是标准模式——Commit 成功则 Rollback 是 no-op,Commit 失败则自动回滚。这确保了原子性。
Q 连接池参数怎么配?
A 核心是 MaxOpenConns(最大并发)和 MaxIdleConns(空闲保持)。建议 MaxOpenConns=并发的 2-3 倍,MaxIdleConns=MaxOpenConns/2。SetConnMaxLifetime=5min 防止连接被数据库中间件断开。
Q SQLite 和 MySQL 在 Go 中用法一样吗?
A 基本一样——都通过 database/sql 接口操作。区别:(1) 占位符 SQLite 用 ?,MySQL 用 ?,PostgreSQL 用 $1;(2) SQL 语法差异(如自增主键写法);(3) 并发性能 SQLite 不如 MySQL。
Q rows.Next() 遍历完后还需要 Close 吗?
A 需要。即使遍历完所有行,rows 仍然持有数据库连接。rows.Close() 释放连接回池。使用 defer rows.Close() 在 rows 创建后立即注册,确保任何路径都释放资源。
Q 如何调试慢 SQL?
A (1) 在 database/sql 层设置 db.SetMaxOpenConnsdb.SetMaxIdleConns 确保连接池不成为瓶颈;(2) 用数据库自身慢查询日志;(3) 用 EXPLAIN ANALYZE 分析查询计划;(4) 考虑添加 SQL 日志中间件记录所有查询耗时。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建 SQLite 数据库,建一个 tasks 表(id, title, done, created_at)。实现 InsertTask、ListTasks(按日期排序)、MarkDone(按 id 更新)三个函数。

  2. 进阶题(难度⭐⭐):实现一个博客系统的数据库层。要求:(1) posts 表(id, title, content, author_id, created_at);(2) 支持分页查询(LIMIT/OFFSET);(3) 预编译插入防注入;(4) 事务:发布文章时同时更新作者的文章数量;(5) 使用 -race 验证并发安全。

  3. 挑战题(难度⭐⭐⭐):实现一个库存管理系统的并发安全操作。要求:(1) 100 个 goroutine 同时下单(每个扣减库存不同商品);(2) 使用 database/sql 事务 + 应用层 Mutex 防止超卖;(3) 预编译所有 SQL;(4) 连接池配置合理;(5) 统计成功/失败的订单数和最终库存。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏