SQL

database/sql 是抽象层,不是数据库实现

刚接触时最容易困惑的一点是:为什么连接 PostgreSQL 用的是标准库的 sql.Open

db, dbErr = sql.Open("pgx", cfg.Database.PostgresDSN)

这里的 sql 是标准库 database/sql 的简称。它不是某个数据库的实现,而是一层「统一接口 + 连接池」。真正和 PostgreSQL 通信的是 pgx 驱动。分层关系如下:

你的代码            db.Query / db.Exec / db.Begin
   ↓
database/sql        标准库:统一 API + 连接池,不含任何数据库实现
   ↓
pgx 驱动            stdlib 适配层:实现 driver.Driver,走 PostgreSQL 协议
   ↓
PostgreSQL 数据库

两个参数各是什么

  • 第一个参数 "pgx" 是驱动名,不是协议也不是数据库类型。database/sql 内部维护一张驱动表(driver name → driver 实现),sql.Open 按这个名字去查表。
  • 第二个参数是 DSN(数据源名称),对 database/sql 而言完全是不透明字符串,它原样转交给驱动的 Open(dsn),由驱动自己解析。

驱动名是怎么注册进去的

空导入——导入驱动包但不使用它的任何导出符号,只为触发它的 init()

import (
	"database/sql"
	_ "github.com/jackc/pgx/v5/stdlib" // 注册驱动名为 "pgx"
)

stdlib 包的 init() 里会执行 sql.Register("pgx", ...),把实现注册进驱动表,sql.Open 才能找到它。

最容易踩的坑:忘了写这个空导入。编译能通过,运行时才报错:

sql: unknown driver "pgx" (forgotten import?)

换驱动只需改驱动名和导入,业务代码不用动——这正是这层抽象的价值。常见对照:

驱动包驱动名
github.com/jackc/pgx/v5/stdlibpgx
github.com/lib/pqpostgres
github.com/go-sql-driver/mysqlmysql

DSN

DSN 就是连接串,例如(也正是 onboard 流程里交互式输入拼装、写入 GOCLAW_POSTGRES_DSN 的那个,见 交互输入opFile):

postgres://user:password@localhost:5432/goclaw?sslmode=disable

陷阱:Open 并不连数据库

sql.Open 只校验参数、创建连接池对象,不会建立任何网络连接。连接是懒建立的——第一次真正执行查询时才拨号。所以 DSN 写错、数据库没起来,Open 照样返回 nil 错误。必须显式探活:

db, err := sql.Open("pgx", cfg.Database.PostgresDSN)
if err != nil {
	return err // 这里只可能是参数或驱动名错误
}
if err := db.Ping(); err != nil {
	return fmt.Errorf("ping postgres: %w", err) // 真正的连通性校验在这里
}

db 用完要关闭,通常配合 defer

defer db.Close()

连接池配置

默认无上限,高并发下容易把数据库打挂,建议显式配置:

db.SetMaxOpenConns(25)                 // 最大打开连接数
db.SetMaxIdleConns(5)                  // 最大空闲连接数
db.SetConnMaxLifetime(30 * time.Minute) // 连接可复用的最长时间

和 Gorm 的关系

DataBase 里 MySQL 用的是 gorm.Open(mysql.Open(dsn), ...)。Gorm 是 ORM,底层仍然是 database/sql + 驱动——gorm.io/driver/postgres 内部用的正是 pgx。

所以 sql.Open("pgx", dsn) 是「裸写」的等价写法:少一层 ORM 映射,需要自己处理 rows.Scan

rows, err := db.Query("SELECT address, tokenid FROM owners WHERE tokenid > ?", 100)
if err != nil {
	return err
}
defer rows.Close()

for rows.Next() {
	var address string
	var tokenID int64
	if err := rows.Scan(&address, &tokenID); err != nil {
		return err
	}
	fmt.Println(address, tokenID)
}
// 遍历结束后必须检查一次错误,区分「正常结束」和「迭代中出错」
if err := rows.Err(); err != nil {
	return err
}

注意占位符是驱动相关的:MySQL 用 ?,PostgreSQL 用 $1, $2。传参而不是拼字符串,可以避免 SQL 注入。

补充:pgx 的原生 API

pgx 还有一套原生 APIpgxpool.New(ctx, dsn)),不走 database/sql,性能更好、对 PostgreSQL 特有类型(数组、jsonb、自定义类型)支持更完整。

sql.Open("pgx", ...) 通常是为了兼容既有代码或统一接口;新项目如果确定只用 PostgreSQL,可以直接上 pgxpool

原生 database/sql 的写入示例(MySQL)

上面都是查询,写入同理——database/sqldb.Exec 执行写语句,结果取回 LastInsertId()(自增主键)和 RowsAffected()(受影响行数),而不是 Gorm 的 result.RowsAffected

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

// dsn 与 gorm 的 mysql.Open 共用同一套格式;Open 只建池不拨号,务必 Ping
db, err := sql.Open("mysql", dsn)
if err != nil {
	return fmt.Errorf("init db: %w", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
	return fmt.Errorf("ping db: %w", err)
}

// 单条插入:? 是 MySQL 占位符,参数化避免 SQL 注入
result, err := db.Exec(
	"INSERT INTO owners (address, owner, tokenid) VALUES (?, ?, ?)",
	"0xabc", "Sam", 1,
)
if err != nil {
	return fmt.Errorf("insert: %w", err)
}
id, _ := result.LastInsertId()
n, _ := result.RowsAffected()
log.Printf("inserted id=%d, rows=%d", id, n)

// 批量插入:预编译一次,循环 Exec 复用
stmt, err := db.Prepare("INSERT INTO owners (address, owner, tokenid) VALUES (?, ?, ?)")
if err != nil {
	return fmt.Errorf("prepare: %w", err)
}
defer stmt.Close()
for i := 1; i < 5; i++ {
	if _, err := stmt.Exec("0xabc", fmt.Sprintf("Sam%d", i), int64(i)); err != nil {
		log.Printf("exec %d failed: %v", i, err)
	}
}

占位符随驱动而异:MySQL 用 ?,PostgreSQL(pgxdatabase/sql)用 $1/$2。上面这些读写示例覆盖了「驱动注册 / 空导入 / 连接池 / 占位符 / OpenPing 陷阱」等全部通用机制;MySQL 的 Gorm 写法(AutoMigratedb.CreateCreateInBatches)见 MySQL 写操作

何时用 Gorm,何时用原生 database/sql

  • 用 Gorm:想要结构体↔表映射、AutoMigrate 建表、链式查询、批量插入开箱即用,少写样板代码。代价是多一层依赖与抽象。
  • 用原生 database/sql:想要最小依赖、完全掌控 SQL 与参数、精确管理预编译/事务,或维护一套跨数据库的统一接口(底层仍是各自驱动)。代价是要自己 rows.Scan、自己拼批量逻辑。

Gorm 并没有替代 database/sql——它底层就是 database/sql + 对应驱动。两者不是二选一,而是「ORM 之上」与「裸接口」的层级关系。