Skip to content

Go 数据库工具选型 ​

pgx、sqlc 和 GORM 经常出现在同一份选型表里,却不负责同一件事:一个主要负责与 PostgreSQL 通信,一个把 SQL 生成 Go 代码,一个提供对象映射和查询接口。把它们当作只能三选一,容易从一开始就选错问题。

本文先分清驱动、查询层和迁移工具,再逐一说明 pgx、go-sql-driver/mysql、sqlx、sqlc、GORM、ent,以及 golang-migrate、goose、Atlas。最后用两个小实验观察参数绑定和零值更新,让“封装方便在哪里、隐藏了什么”变成可以检查的行为。

资料核对于 2026-10-13。实验固定 Go 1.27.2 / Linux amd64、sqlx v1.4.0、GORM v1.31.1、GORM SQLite 驱动 v1.6.0、实际 SQLite 引擎 3.45.1;完整依赖与校验值见附录。实验范围只包括 sqlx 文本绑定和 GORM 对本地内存 SQLite 的操作,不代表 PostgreSQL、MySQL 的事务、锁或生产性能。其余工具依据官方文档讲解,本次没有运行其生成器、迁移器或真实数据库连接。

1. 先选层,再选工具 ​

层次负责什么本文工具不替你解决什么
数据库驱动与协议建连、认证、传输 SQL 与参数、解码结果pgx、go-sql-driver/mysql业务模型、查询质量、迁移计划
SQL 辅助层简化绑定、扫描与样板代码sqlx自动理解任意关系、编译期检查所有 SQL
SQL 代码生成从 schema 和 SQL 生成类型化方法sqlc自动选最优索引、自动应用 schema
ORM / 模型生成从模型构造 CRUD 和关联操作GORM、ent业务不变量、所有性能问题、无风险改表
schema 迁移记录并推进结构变更golang-migrate、goose、Atlas保证所有 DDL 都在线、丢失数据都能恢复

常见组合可以是“sqlc + pgx”,也可以是“sqlx + database/sql + MySQL 驱动”。迁移工具在部署路径上工作,不需要与每条业务查询绑定。

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart TD
    SQL[Schema 与手写 SQL] --> GEN[sqlc 生成 Go 方法]
    MODEL[Go 模型或 ent schema] --> ORM[GORM 或 ent 查询层]
    APP[业务逻辑] --> GEN
    APP --> X[手写 SQL 或 sqlx]
    APP --> ORM
    GEN --> N[pgx 原生接口]
    GEN --> STD[database/sql]
    X --> STD
    ORM --> STD
    N --> PG[PostgreSQL]
    STD --> PD[pgx stdlib 适配器]
    PD --> PG
    STD --> MYD[go-sql-driver/mysql]
    MYD --> MY[MySQL]
    MIG[独立迁移发布流程] --> PG
    MIG --> MY
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart TD
    SQL[Schema 与手写 SQL] --> GEN[sqlc 生成 Go 方法]
    MODEL[Go 模型或 ent schema] --> ORM[GORM 或 ent 查询层]
    APP[业务逻辑] --> GEN
    APP --> X[手写 SQL 或 sqlx]
    APP --> ORM
    GEN --> N[pgx 原生接口]
    GEN --> STD[database/sql]
    X --> STD
    ORM --> STD
    N --> PG[PostgreSQL]
    STD --> PD[pgx stdlib 适配器]
    PD --> PG
    STD --> MYD[go-sql-driver/mysql]
    MYD --> MY[MySQL]
    MIG[独立迁移发布流程] --> PG
    MIG --> MY

图中的线是可选组合,不是要求同时接入所有层。database/sql 本身提供连接池和统一接口,但不包含数据库驱动;查询层的抽象也不会让不同数据库的 SQL、类型和隔离行为自动一致。[1]

2. 驱动:先确定连接池由谁管理 ​

pgx:PostgreSQL 原生能力与标准接口之间的选择 ​

pgx 是 PostgreSQL 驱动及工具集。它有原生 API,也提供 stdlib 适配器接入 database/sql。原生接口可以直接使用 PostgreSQL 特有能力,如 COPY、LISTEN / NOTIFY、批量请求及更丰富的类型映射;若上层库要求 *sql.DB,则可以选择适配器。[2]

接法池在哪里适合什么情况要留意什么
pgx.Conn这是单连接,不是池专用连接、低层协议操作不能把同一连接当作可并发共享的池
pgxpool.Poolpgxpool 管理PostgreSQL 专用应用,直接用原生 API参数、统计和事务 API 与 sql.DB 不完全相同
pgx stdlib + *sql.DBdatabase/sql 管理sqlx、ORM 或已有标准接口代码原生能力不全都能从标准接口直接表达

连接池应长期复用,而不是每个请求建一个。pgxpool.New 返回也不表示已经成功建连,应通过带超时的 Ping 或实际获取连接验证。使用原生事务时,还要阅读其 Context 语义:例如 pgxpool 的 Begin 所用 Context 不等于事务在 Context 取消后自动回滚,调用方仍应明确结束事务。不要把 database/sql 的生命周期印象直接套过来。[3][4]

若数据库前面有连接代理,应进一步核实所选查询模式、预备语句缓存和代理池化模式是否兼容。决定采用原生接口,应该因为确实需要它的能力或测得收益,而不是引用一个脱离负载的“必定快几倍”。本文没有做驱动性能对比。

go-sql-driver/mysql:在标准池之下实现 MySQL 协议 ​

go-sql-driver/mysql 是 database/sql 的 MySQL 驱动,业务代码通常通过 *sql.DB 使用。驱动注册名为 mysql;连接数、空闲连接和连接寿命主要在 sql.DB 上配置,不是另外再找一个“mysql 池”。[5]

配置中有几个容易混淆的层次:

设置改变什么不能据此推断什么
parseTime=true将 DATE、DATETIME 解码为 time.Time不代表数据库会话时区已修改
loc上述 Go 时间值采用的位置不修改 MySQL 的 time_zone
timeout / readTimeout / writeTimeout建连或 I/O 层超时不等于完整业务请求的统一截止时间
TLS 配置传输加密和证书校验方式开启连接不等于可以省略身份校验
参数绑定值与 SQL 结构分开传给驱动参数占位符不能替代任意表名、列名或排序方向

尤其是时间:数据库里一列表示的是 UTC 时刻、本地日历时间还是日期,需要应用先定协议,再配置解码。parseTime 不能替你决定业务时区。DSN 还可能包含密码,不应完整打印到日志。[5:1]

3. sqlx:保留 SQL,把重复扫描和绑定交给库 ​

sqlx 在 database/sql 之上增加 Get、Select、StructScan、命名参数和 IN 展开等功能。SQL 仍由你写;它不会根据 Go 对象关系自动决定 JOIN,也不是编译期 SQL 检查器。[6]

例如一条查询返回 id 和 display_name,目标结构体可以用 db 标签映射。JOIN 中有两个 id 时,应显式写不同列别名,而不是期望反射推断“这个 id 属于哪张表”。默认找不到目标字段会报错;随手打开 Unsafe 只会掩盖映射问题,不会修复 SQL。[6:1]

一个 slice 不会自动变成一串占位符 ​

假设查询有一个布尔条件和两个用户 ID。实验按顺序执行:命名参数转 ?,展开 slice,再转换成 PostgreSQL 风格的 $n。

go
query, args, err := sqlx.Named(
    "SELECT id FROM users WHERE active=:active AND id IN (:ids)",
    map[string]any{"active": true, "ids": []int{2, 7}},
)
// 完整程序检查每次调用的 err。
query, args, err = sqlx.In(query, args...)
query = sqlx.Rebind(sqlx.DOLLAR, query)

实际输出:

text
sqlx: SELECT id FROM users WHERE active=$1 AND id IN ($2, $3) args=[true 2 7]
sqlx: empty-slice-error=true

SQL 里增加了两个位置,参数仍是独立的值。这一步只生成文本与参数,没有连接 PostgreSQL。空 slice 被 sqlx.In 拒绝,调用方应先决定“空集合表示不返回任何行”还是“忽略此条件”,不能把空集合错误当成数据库异常随意重试。该行为在 v1.4.0 的 bind.go 中也可核对。[7]

Rebind 只改占位符形式,不把 SQL 方言整体翻译成另一个数据库。动态表名、排序列仍需可信白名单,不能把用户文本拼进去。读大量数据时也别把 Select 当成无上限的便利函数:一次装入切片会增加内存占用,需要时使用逐行扫描,并检查关闭与迭代错误。[6:2]

4. sqlc:把手写 SQL 变成有类型的 Go 调用 ​

sqlc 的起点是数据库 schema 和带命名注释的 SQL。生成器分析这些输入,生成参数结构、结果类型和查询方法。应用运行时调用生成后的 Go 代码,仍通过配置的数据库接口执行原来的查询;不是每个请求都启动 sqlc。[8]

可以把它理解为把这条链提前到构建阶段:

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    S[Schema 定义] --> C[sqlc 分析与生成]
    Q[命名 SQL 查询] --> C
    C --> G[参数类型 结果类型 查询方法]
    G --> B[Go 编译与测试]
    B --> R[运行时驱动执行 SQL]
    D[实际数据库 schema] --> V[集成测试与迁移校验]
    R --> V
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    S[Schema 定义] --> C[sqlc 分析与生成]
    Q[命名 SQL 查询] --> C
    C --> G[参数类型 结果类型 查询方法]
    G --> B[Go 编译与测试]
    B --> R[运行时驱动执行 SQL]
    D[实际数据库 schema] --> V[集成测试与迁移校验]
    R --> V

它解决的是“我愿意写 SQL,但不想手工维护每条查询的 Scan 和参数样板”。复杂 JOIN、CTE 或数据库特有表达式可以保留在 SQL 中,评审者能直接审查询。与此同时,生成器有自己的语法、类型推断和数据库支持边界;遇到自定义类型或特殊查询,要检查配置、类型覆盖及该版本能力,不应承诺任意合法 SQL 都一定可生成。[9]

采用 sqlc 后的工作为什么仍然需要
固定生成器版本和配置同样输入要产生可复核的输出,避免本地和 CI 不同
修改查询后重新生成、编译并检查差异Go 方法和结果类型可能随列变化
对真实目标数据库做集成测试输入 schema 不等于线上 schema;编译成功不能发现所有漂移
查看执行计划和行数类型安全不表示查询代价合适
显式管理事务生成查询不会自动把多次调用合成业务事务

sqlc 生成的 WithTx 可以把查询对象绑定到事务。关键是后续调用都使用这个事务对象;如果事务中误用原来的池查询对象,调用可能落在另一条连接上。WithTx 不负责替你选择隔离级别、处理提交失败或重试完整业务操作。[10]

sqlc 更适合查询形状相对明确、团队愿意审 SQL 的项目。高度动态的筛选条件也能设计,但常要选择有限的查询变体、谨慎的可选条件或另一个受控构造层。不要为了“所有查询必须由同一种工具生成”而写出无法维护的大 SQL。

5. GORM:用模型组织 CRUD,也要看它实际写了什么 ​

GORM 提供模型映射、条件构造、关联、回调、事务与迁移接口。它能减少常见 CRUD 的重复代码,但调用的是有约定的 API,并非把一个结构体原封不动地同步到数据库。GORM 也有泛型接口;下面固定使用 v1.31.1 的传统 API,以便清楚观察其更新规则。[11][12]

false 究竟表示“改为假”,还是“没填写”? ​

实验先插入一名 Active=true 的用户,然后尝试:

go
db.Model(&User{}).Where("id = ?", 1).
    Updates(User{Active: false})

实际读回:

text
gorm struct: active=true affected=0

调用没有报错,但值没变。传统 API 的 Updates(struct) 默认选择非零值字段,false 是 bool 的零值,因此被跳过。程序没有 CreatedAt / UpdatedAt 等额外字段,所以这次没有其他更新。

改成明确的字段 map:

go
db.Model(&User{}).Where("id = ?", 1).
    Updates(map[string]any{"active": false})

实际读回及 DryRun 输出:

text
gorm map: active=false affected=1
gorm SQL: UPDATE `users` SET `active`=? WHERE id = ? vars=[false 1]

也可以显式 Select 要更新的字段。这里更重要的设计问题是:API 输入能否区分“字段没有提供”和“提供了 false / 0 / 空串”?必要时使用指针或专门的变更类型,在应用层保留这个区别,再决定如何调用 ORM。不要只因为代码编译通过,就认定更新语义正确。[12:1]

关联和便利功能也有成本 ​

Preload 通常另发查询批量加载关联,并不等于“一条 JOIN”。这能避免逐父记录查一次的 N+1,但可能返回很多关联行;Join Preload 又有关系类型等适用边界。应看实际 SQL、查询次数和结果规模,而不是按链式调用写了几行估计成本。[13]

GORM 的参数值可以绑定,但 Order、Table 等接受 SQL 结构的入口不能直接接不可信文本。日志里展示的 SQL 也不应未经核实就复制执行。ORM 能帮助构造查询,不能替代输入边界。[14]

AutoMigrate 能创建或调整某些 schema 项,官方也明确它不会为了匹配模型而删除不用的列。但“不删列”不等于变更无锁、无重写或适合每个实例启动时同时执行。本文只在临时 SQLite 中用它准备实验表;生产结构变更应有独立评审和执行流程。[15]

6. ent:先定义 schema,再生成领域数据 API ​

ent 的核心工作流是用 Go 定义 schema,包括字段、索引和 edges,然后生成类型化的实体与查询构造 API。edges 描述实体间的关系,例如用户拥有多个订单、订单属于一个用户;在 SQL 后端,这些关系最终仍要由表、键和查询实现,不是引入一个可以免费跨关系遍历的内存图。[16][17]

它与 sqlc 的方向不同:sqlc 从你写的 SQL 生成调用;ent 从模型及关系定义生成可组合 API。若领域关系丰富、团队希望字段与关系变更尽早反映到 Go 编译错误中,ent 的方式很有吸引力。它也有 runtime、查询构造和驱动调用,不能因为使用代码生成就称为“没有运行时成本”。

实际收益相应取舍
字段、谓词和关系遍历有生成 API要维护生成步骤,评审 schema 与生成差异
可以围绕模型组织校验、hooks 和查询隐含行为需被团队理解,排查时仍要回到实际 SQL
与 sql.DB 集成,复用连接管理池参数和底层驱动仍需配置,不会自动消失
支持事务和版本化迁移集成业务事务边界、锁代价和发布顺序仍需设计

生成代码应视为构建产物:修改 schema 后重新生成,不要把手工修补生成文件当成长久方案。使用事务客户端时,确保一个工作单元使用同一事务范围;复杂分析 SQL 或特殊数据库能力是否容易表达,应在选型时做小型验证,而不是等项目已经绑定大量模型后才发现。[18][19][20]

7. 迁移工具:它们管理变更过程,不替数据库改变 DDL 规则 ​

数据库 schema 会随应用演进。迁移工具至少要回答:哪些变更已经执行,下一步执行什么,失败后停在哪儿,多个发布者是否会互相干扰。它们不等于 ORM,也不要求应用一定用 Go。

工具核心工作方式合适的需求主要边界
golang-migrate按版本执行成对的 up / down 文件,CLI 或 Go 库团队想直接审 SQL,执行器职责清晰失败 dirty 状态需调查;事务行为取决于数据库及驱动
goose有注释分区的 SQL 迁移,也支持 Go 迁移SQL 之外确实需要受控程序逻辑自定义程序更难审;事务例外要明确标注
Atlas声明式比较目标状态,或生成并执行版本化迁移希望把 schema 差异、检查和发布组织起来生成的差异仍需审查;具体能力与版本、发行及服务方案有关

golang-migrate:版本号不能冒充修复 ​

常见文件是一对带相同版本号的 .up.sql 和 .down.sql。工具记录当前版本与 dirty 状态;迁移失败后停下来,避免在不明结构上继续叠加变更。[21]

遇到 dirty,先检查日志、实际 schema 和已生效语句,确定恢复到哪种一致状态,再决定补救和版本标记。force 的意义是设置迁移版本状态,不会替你执行遗漏的 SQL,也不会修复半完成的数据回填。把 dirty 清掉后继续跑,可能让工具记录与真实数据库永久分叉。[22]

事务更不能一概而论。例如 PostgreSQL 驱动文档对多语句执行以及 CREATE INDEX CONCURRENTLY 的事务限制有专门说明;工具名字相同,不代表每个数据库都能把同一份脚本完整回滚。[23]

goose:SQL 注释划边界,Go 迁移承担额外责任 ​

goose 的 SQL 文件通过 -- +goose Up 和可选的 -- +goose Down 区分方向。包含函数体等内部有分号的完整语句时,需要按规则用 StatementBegin / StatementEnd 标注,让解析器知道这是一条语句,而不是按每个分号拆开。[24]

默认一个 SQL 迁移文件内的语句放进事务;需要在事务外运行的操作可以声明 NO TRANSACTION。但数据库 DDL 自身的隐式提交等规则仍然适用,注释不能制造数据库没有的原子性。关闭事务后,更应逐条考虑失败会留下什么状态。[24:1]

Go 迁移可以表达 SQL 难以单独完成的数据转换,却也可能混入不可重复的外部调用。建议把大规模回填拆成可分批、可续跑、可校验的任务,避免在一次发布的 schema 迁移中长时间占锁。是否需要 Go 迁移,应由具体转换需求决定,而不是为了把全部数据工作放进一个工具。[25]

Atlas:目标状态和有序步骤,是两种工作流 ​

声明式迁移描述“希望数据库是什么样”,工具比较实际状态并规划差异;版本化迁移保存“按什么步骤到达那里”,便于评审和部署。Atlas 支持这两条路径,也能把模型提供的 schema 接入规划。生产流程常需要先生成可检查的版本化 SQL,而不是让任意应用启动时临场推断所有变更。[26][27]

atlas.sum 用迁移文件及目录校验信息检测历史被意外修改等问题。它有助于维护迁移目录的一致性,但正确的校验和不证明 SQL 无损、不证明线上没有漂移,也不证明新旧应用兼容。自动 diff 更无法替你决定“删掉一列”到底是可以丢弃,还是一次需要迁移数据的重命名意图。[28]

从哪些入口开始操作 ​

下面是工作流入口,不是可直接复制到生产执行的命令串;连接地址、目标目录、权限和版本必须先确认。

工具开始时看什么推进变更故障时先做什么
golang-migrate检查版本和文件顺序按批准的步数执行 up;需要时明确指定版本查看 dirty 与实际 schema,确认恢复方案后才考虑 force
goosestatus 查看已执行记录up 推进,down 只在已评审反向操作时使用分清事务内失败还是已部分执行;不要只盯版本表
Atlasinspect / diff 查看实际与目标;审查生成文件版本化流程使用 migrate 的检查与 apply 入口检查迁移历史、目录校验和与实际结构;不要盲目重算校验和掩盖改动

生产发布的共同底线 ​

把字段从 A 改为 B 时,不要先删 A、再期待所有应用同时升级。可采用扩展再收缩:先增加 B 并允许旧版本继续工作,部署兼容读写,分批回填和校验,切换读取,确认旧版本与回滚窗口结束后再清理 A。这是发布策略,不是某个迁移工具自动保证的功能。

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    A[添加兼容结构] --> B[部署兼容代码]
    B --> C[分批回填与校验]
    C --> D[切换读取]
    D --> E[确认旧版本退出]
    E --> F[单独评审破坏性清理]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    A[添加兼容结构] --> B[部署兼容代码]
    B --> C[分批回填与校验]
    C --> D[切换读取]
    D --> E[确认旧版本退出]
    E --> F[单独评审破坏性清理]

Down 文件也不等于恢复方案:删掉的列即使重新创建,旧值不会凭空回来。回滚应用、反向迁移、向前修复和从备份恢复,是不同动作。发布前应逐一确认锁等待、预计耗时、重试方式、停止条件和恢复证据。

8. 怎样做最后选择 ​

项目主要特征可先验证的组合为什么
PostgreSQL 专用,SQL 清晰且较稳定pgx + sqlc + 一种迁移工具保留 SQL 控制力,减少参数与结果样板
已有大量手写 SQL 和 database/sql 代码原驱动 + sqlx渐进减少扫描与绑定重复,改动范围较小
以常规 CRUD 为主,愿意遵守 ORM 约定GORM + 目标驱动 + 受审查迁移开发入口统一;必须验证更新和关联语义
模型与关系是团队的主要表达方式ent + 目标驱动 + 版本化迁移生成类型化 API,schema 变更更早暴露到构建阶段

这是试用起点,不是“最佳技术栈”排名。最终验证应拿自己的一条复杂查询、一次部分字段更新、一项跨表事务、一条分页路径和一次真实规模的 schema 变更来做。

至少记录:发出了几条 SQL,绑定了哪些参数,返回多少行,池等了多久,事务在哪里结束,迁移失败留下什么。工具是否适合项目,通常会在这些边界上显现,而不是在最短的 CRUD 演示里。

参考资料 ​

附录:完整实验模块 ​

以下所有文件放在同一目录。实验使用 CGO SQLite 驱动,因此需要 C 编译器;依赖首次下载需联网。本次通过官方 Go module proxy 和 checksum database 下载固定依赖,随后关闭依赖下载执行程序、单元测试、race 与 vet。只创建进程内存数据库,不使用凭据或外部服务。

命令如下;已有 Go 1.27.2 时直接运行:

sh
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download
CGO_ENABLED=1 GOPROXY=off GOSUMDB=off go run .
CGO_ENABLED=1 GOPROXY=off GOSUMDB=off go test -v ./...
CGO_ENABLED=1 GOPROXY=off GOSUMDB=off go test -race -count=3 ./...
CGO_ENABLED=1 GOPROXY=off GOSUMDB=off go vet ./...

sqlx.Rebind 的 PostgreSQL 风格结果只作文本验证;GORM SQL 与行值验证使用 SQLite。没有用 SQLite 推断其他数据库的 RowsAffected、锁、隔离或 DDL 行为。DryRun 只显示构造出的语句,不是执行计划。

本次 go test -v ./... 通过 TestBindingsAndZeroValues;go test -race -count=3 ./... 通过,go vet ./... 退出 0、无输出。它们不是性能基准。下面给出产生正文全部输出的源文件。

go.mod ​

go
module example.com/storagetools

go 1.27

require (
	github.com/jmoiron/sqlx v1.4.0
	gorm.io/driver/sqlite v1.6.0
	gorm.io/gorm v1.31.1
)

require (
	github.com/jinzhu/inflection v1.0.0 // indirect
	github.com/jinzhu/now v1.1.5 // indirect
	github.com/mattn/go-sqlite3 v1.14.22 // indirect
	golang.org/x/text v0.20.0 // indirect
)

go.sum ​

text
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

main.go ​

代码块 · 98 行 · 点击收起 / 展开
go
package main

import (
	"fmt"
	"io"
	"os"

	"github.com/jmoiron/sqlx"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

type User struct {
	ID     int `gorm:"primaryKey"`
	Name   string
	Active bool
}

func run(w io.Writer) error {
	query, args, err := sqlx.Named("SELECT id FROM users WHERE active=:active AND id IN (:ids)", map[string]any{"active": true, "ids": []int{2, 7}})
	if err != nil {
		return err
	}
	query, args, err = sqlx.In(query, args...)
	if err != nil {
		return err
	}
	query = sqlx.Rebind(sqlx.DOLLAR, query)
	fmt.Fprintf(w, "sqlx: %s args=%v\n", query, args)
	_, _, emptyErr := sqlx.In("SELECT id FROM users WHERE id IN (?)", []int{})
	fmt.Fprintf(w, "sqlx: empty-slice-error=%t\n", emptyErr != nil)
	if emptyErr == nil {
		return fmt.Errorf("expected empty slice error")
	}

	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
	if err != nil {
		return err
	}
	raw, err := db.DB()
	if err != nil {
		return err
	}
	defer raw.Close()
	raw.SetMaxOpenConns(1)
	var version string
	if err := raw.QueryRow("SELECT sqlite_version()").Scan(&version); err != nil {
		return err
	}
	fmt.Fprintln(w, "SQLite:", version)
	if err := db.AutoMigrate(&User{}); err != nil {
		return err
	}
	if err := db.Create(&User{ID: 1, Name: "Ada", Active: true}).Error; err != nil {
		return err
	}

	result := db.Model(&User{}).Where("id = ?", 1).Updates(User{Active: false})
	if result.Error != nil {
		return result.Error
	}
	var got User
	if err := db.First(&got, 1).Error; err != nil {
		return err
	}
	fmt.Fprintf(w, "gorm struct: active=%t affected=%d\n", got.Active, result.RowsAffected)
	if !got.Active {
		return fmt.Errorf("struct unexpectedly updated zero value")
	}

	result = db.Model(&User{}).Where("id = ?", 1).Updates(map[string]any{"active": false})
	if result.Error != nil {
		return result.Error
	}
	got = User{}
	if err := db.First(&got, 1).Error; err != nil {
		return err
	}
	fmt.Fprintf(w, "gorm map: active=%t affected=%d\n", got.Active, result.RowsAffected)
	if got.Active {
		return fmt.Errorf("map did not update false")
	}

	stmt := db.Session(&gorm.Session{DryRun: true}).Model(&User{}).Where("id = ?", 1).Updates(map[string]any{"active": false})
	if stmt.Error != nil {
		return stmt.Error
	}
	fmt.Fprintf(w, "gorm SQL: %s vars=%v\n", stmt.Statement.SQL.String(), stmt.Statement.Vars)
	return nil
}

func main() {
	if err := run(os.Stdout); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

main_test.go ​

go
package main

import (
	"bytes"
	"strings"
	"testing"
)

func TestBindingsAndZeroValues(t *testing.T) {
	var out bytes.Buffer
	if err := run(&out); err != nil {
		t.Fatal(err)
	}
	for _, want := range []string{
		"sqlx: SELECT id FROM users WHERE active=$1 AND id IN ($2, $3) args=[true 2 7]",
		"sqlx: empty-slice-error=true",
		"gorm struct: active=true affected=0",
		"gorm map: active=false affected=1",
		"vars=[false 1]",
	} {
		if !strings.Contains(out.String(), want) {
			t.Fatalf("missing %q in %s", want, out.String())
		}
	}
}

  1. Go:database/sql。池、驱动与事务接口。 ↩︎

  2. pgx 官方项目。原生能力及 stdlib 适配器。 ↩︎

  3. pgxpool API。池构造、获取和事务语义。 ↩︎

  4. pgx v5.11.0 API。单连接并发与事务 Context 边界。 ↩︎

  5. go-sql-driver/mysql 官方 README。database/sql、DSN、parseTime、loc、超时和 TLS。 ↩︎ ↩︎

  6. sqlx 作者指南。查询、扫描、命名参数与池。 ↩︎ ↩︎ ↩︎

  7. sqlx v1.4.0:bind.go。IN 展开和空 slice 错误;另见本次实际输出。 ↩︎

  8. sqlc v1.31.1 文档。固定文档版本,不表示本文安装了生成器。 ↩︎

  9. sqlc v1.31.1 配置。数据库、sql_package、生成选项与类型覆盖。 ↩︎

  10. sqlc:Using transactions。WithTx。 ↩︎

  11. GORM:The Generics Way。 ↩︎

  12. GORM:Update;v1.31.1 更新实现。结构体零值、map、Select。 ↩︎ ↩︎

  13. GORM:Preloading。独立查询与 Join Preload。 ↩︎

  14. GORM:Security。值绑定和 SQL 结构入口。 ↩︎

  15. GORM:Migration。AutoMigrate 与版本化流程。 ↩︎

  16. ent:Code Generation。 ↩︎

  17. ent:Edges。 ↩︎

  18. ent:Transactions。 ↩︎

  19. ent:sql.DB Integration。 ↩︎

  20. ent:Versioned Migrations。 ↩︎

  21. golang-migrate 官方项目。文件、CLI 与库。 ↩︎

  22. golang-migrate FAQ。dirty、版本和锁。 ↩︎

  23. golang-migrate PostgreSQL 驱动说明。多语句与事务限制。 ↩︎

  24. goose:SQL file annotations。 ↩︎ ↩︎

  25. goose 官方文档。SQL 与 Go 迁移。 ↩︎

  26. Atlas:Declarative vs Versioned。 ↩︎

  27. Atlas:Versioned Migrations。 ↩︎

  28. Atlas:Migration Directory Integrity。atlas.sum 的作用与范围。 ↩︎

保留各书来源与许可 · 示例运行条件以正文为准