Golang database/sql 标准库基础

database/sql 标准库基础

核心概念

Go 标准库 database/sql 是一个接口与规范层 ,它定义了操作数据库的通用 API(连接池管理、预处理、事务、数据绑定等),但不包含任何具体数据库协议的实现。真正干活的是数据库驱动 ------第三方包通过 sql.Register()init() 中把自己注册进来。

这种设计意味着:无论底层是 MySQL、PostgreSQL 还是 SQLite,上层代码只需面对 database/sql 的统一接口,切换数据库只需换一行 import 和连接字符串。

架构分层

复制代码
你的应用代码
    ↓
database/sql (标准库:接口 + 连接池 + 事务抽象)
    ↓
database/sql/driver (驱动接口定义)
    ↓
具体驱动 (go-sql-driver/mysql, lib/pq, modernc.org/sqlite ...)
    ↓
数据库服务器

驱动注册机制

go 复制代码
import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"  // 前缀 _ 表示只执行 init()
)

_ "驱动包路径" 是 Go 的惯用写法:不直接引用包内的任何标识符,但触发其 init() 函数。驱动包的 init() 内部调用 sql.Register("mysql", &MySQLDriver{}),把自己注册到 database/sql 的全局驱动表里。

sql.DB:连接池的抽象

sql.Open() 返回的 *sql.DB 不是单个连接,而是一个连接池管理器。它内部维护着一组空闲连接,按需创建和回收。

核心方法一览

方法 用途 返回值
sql.Open(driver, dsn) 创建连接池配置(懒连接,不立即建连) *sql.DB, error
db.Ping() 实际建立连接,验证可达性 error
db.Close() 关闭连接池,释放所有资源 error
db.Query(sql, args...) 执行查询,返回多行 *sql.Rows, error
db.QueryRow(sql, args...) 执行查询,返回单行 *sql.Row(无 error)
db.Exec(sql, args...) 执行写操作(INSERT/UPDATE/DELETE) sql.Result, error

连接池关键参数

go 复制代码
db.SetMaxOpenConns(25)      // 最大同时打开的连接数(0 = 无限制)
db.SetMaxIdleConns(10)      // 最大空闲连接数(默认 2,偏低)
db.SetConnMaxLifetime(5 * time.Minute)  // 连接最大存活时间
db.SetConnMaxIdleTime(2 * time.Minute)  // 连接最大空闲时间

为什么需要 ConnMaxLifetime? 数据库服务器和中间件(如 ProxySQL)通常会主动断开长时间空闲的连接。如果不设生命周期,Go 连接池里的连接可能已经被服务端关闭,下次使用时才会报错。设置一个比服务端超时更短的值,让连接池主动回收。

三种查询姿势

1. Query:多行查询

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

for rows.Next() {
    var id int
    var name string
    if err := rows.Scan(&id, &name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ID: %d, Name: %s\n", id, name)
}
if err := rows.Err(); err != nil {  // 检查遍历过程中是否有错
    log.Fatal(err)
}

三个必做动作defer rows.Close()rows.Next() 循环 → 最后检查 rows.Err()。遗漏任何一个都是 Bug。

2. QueryRow:单行查询

go 复制代码
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows {
        fmt.Println("用户不存在")
    } else {
        log.Fatal(err)
    }
}

QueryRow 不直接返回 error,error 被延迟到 Scan() 时才暴露。这样设计是为了链式调用。注意 sql.ErrNoRows 是预期内的"没找到",不应当视为系统错误。

3. Exec:写操作

go 复制代码
result, err := db.Exec("INSERT INTO users(name, age) VALUES(?, ?)", "张三", 25)
if err != nil {
    log.Fatal(err)
}
id, _ := result.LastInsertId()    // 自增主键
affected, _ := result.RowsAffected() // 影响行数

完整练习代码

以下代码使用 SQLite(modernc.org/sqlite,纯 Go 实现,无需 CGO),可以直接运行。

go 复制代码
// database_sql_basics.go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"time"

	_ "modernc.org/sqlite"
)

func main() {
	// 1. 打开数据库连接池
	// sql.Open 不会立即建立连接,只是初始化连接池配置
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		log.Fatalf("打开数据库失败: %v", err)
	}
	defer db.Close()

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

	// 3. Ping 验证连接是否可达
	if err := db.Ping(); err != nil {
		log.Fatalf("数据库连接失败: %v", err)
	}
	fmt.Println("=== 数据库连接成功 ===")

	// 4. 建表
	schema := `
	CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		name TEXT NOT NULL,
		email TEXT UNIQUE,
		age INTEGER DEFAULT 0,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP
	)`
	if _, err := db.Exec(schema); err != nil {
		log.Fatalf("建表失败: %v", err)
	}
	fmt.Println("=== 建表成功 ===")

	// 5. 插入数据 (Exec)
	result, err := db.Exec(
		"INSERT INTO users(name, email, age) VALUES(?, ?, ?)",
		"张三", "zhangsan@example.com", 28,
	)
	if err != nil {
		log.Fatalf("插入失败: %v", err)
	}
	lastID, _ := result.LastInsertId()
	affected, _ := result.RowsAffected()
	fmt.Printf("插入成功: ID=%d, 影响行数=%d\n", lastID, affected)

	// 批量插入
	for _, u := range []struct{ name, email string; age int }{
		{"李四", "lisi@example.com", 32},
		{"王五", "wangwu@example.com", 25},
		{"赵六", "zhaoliu@example.com", 35},
	} {
		db.Exec("INSERT INTO users(name, email, age) VALUES(?, ?, ?)", u.name, u.email, u.age)
	}

	// 6. 单行查询 (QueryRow)
	fmt.Println("\n=== 单行查询 ===")
	var name, email string
	var age int
	err = db.QueryRow("SELECT name, email, age FROM users WHERE id = ?", 1).Scan(&name, &email, &age)
	if err != nil {
		if err == sql.ErrNoRows {
			fmt.Println("未找到 ID=1 的用户")
		} else {
			log.Fatal(err)
		}
	} else {
		fmt.Printf("ID=1: %s, %s, %d岁\n", name, email, age)
	}

	// 7. 多行查询 (Query)
	fmt.Println("\n=== 多行查询: age > 26 ===")
	rows, err := db.Query("SELECT id, name, age FROM users WHERE age > ? ORDER BY age", 26)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	for rows.Next() {
		var id, rAge int
		var rName string
		if err := rows.Scan(&id, &rName, &rAge); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("  ID=%d, %s, %d岁\n", id, rName, rAge)
	}
	if err := rows.Err(); err != nil {
		log.Fatal(err)
	}

	// 8. 更新
	fmt.Println("\n=== 更新操作 ===")
	res, err := db.Exec("UPDATE users SET age = ? WHERE name = ?", 29, "张三")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("更新影响行数: %d\n", affected2(res))

	// 9. 删除
	fmt.Println("\n=== 删除操作 ===")
	delRes, err := db.Exec("DELETE FROM users WHERE name = ?", "赵六")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("删除影响行数: %d\n", affected2(delRes))

	// 10. 查看最终数据
	fmt.Println("\n=== 最终用户列表 ===")
	rows2, _ := db.Query("SELECT id, name, email, age FROM users ORDER BY id")
	defer rows2.Close()
	for rows2.Next() {
		var id, rAge int
		var rName, rEmail string
		rows2.Scan(&id, &rName, &rEmail, &rAge)
		fmt.Printf("  [%d] %s <%s> %d岁\n", id, rName, rEmail, rAge)
	}
}

func affected2(res sql.Result) int64 {
	n, _ := res.RowsAffected()
	return n
}

运行方式

bash 复制代码
mkdir -p demo && cd demo
go mod init demo
go get modernc.org/sqlite
# 将上面的代码保存为 main.go,然后:
go run main.go

要点总结

  1. database/sql 是接口层 ,真正通信靠驱动包;_ "驱动路径" 触发 init() 完成注册
  2. sql.DB 是连接池 ,不是单个连接;sql.Open() 是懒连接,db.Ping() 才真正建连
  3. 连接池四个参数 必须根据业务场景合理配置,特别是 ConnMaxLifetime 要比 DB 服务端超时短
  4. Query/QueryRow/Exec 三板斧 :Query 返回 *sql.Rows(必须 Close),QueryRow 返回 *sql.Row(Scan 时才报错),Exec 返回 sql.Result(可取 LastInsertId/RowsAffected)
  5. defer rows.Close() 不是可选项------遗漏会导致连接泄漏,连接池耗尽后整个服务卡死
相关推荐
zhanghaha13141 小时前
Python进阶教程:13_math 模块 —— 新手完全指南
数据库·python·机器学习
__zRainy__1 小时前
Node系列 · 数据库:单表查询
数据库·后端·mysql·node.js
xcLeigh2 小时前
聊聊数据库迁移工具怎么从单机走向“云+端+服务”,KDMS架构拆解
数据库·架构·数据库迁移·kes·kdms·架构拆解
St_rive2 小时前
selenium cookie的处理
数据库·selenium·测试工具
北漂燕郊杨哥2 小时前
CAD Mini Viewer —— 一个纯 Go 编写的轻量 DXF 查看器
golang·cad·dxf·cadview
辻弋2012 小时前
一键优化电源计划、游戏模式、独显强制、禁用后台录制——DeltaForceBooster专治三角洲行动卡顿掉帧,所有改动可一键还原
服务器·数据库·windows·游戏·电脑·php
xcLeigh3 小时前
Go入门:基本数据类型全览与选择指南
android·服务器·golang·教程·go热门
Wang's Blog3 小时前
PostgreSQL笔记51: 基于 pgvector 构建企业级智能问答系统
数据库·笔记·postgresql
我星期八休息3 小时前
Linux—五种IO模型与非阻塞IO
linux·运维·服务器·网络·数据库·网络协议