必须用db.query或db.queryrow调用存储过程,因exec无法获取out参数和结果集;out/inout参数需通过@var用户变量中转,并在同连接上额外执行select读取。

database/sql 是 Go 调用 MySQL 存储过程最直接、最可控的方式,不需要 ORM 封装层干扰参数绑定和结果集处理逻辑。
call 语句必须用 db.Query 或 db.QueryRow,不能用 db.Exec
MySQL 存储过程即使不返回结果集(比如只做 INSERT/UPDATE),只要定义了 OUT 或 INOUT 参数,就必须用查询类方法执行 —— 因为 db.Exec 会忽略所有输出参数,且无法读取存储过程内部的 SELECT 结果集。
常见错误现象:sql: expected 0 arguments, got 2 或调用后 OUT 变量始终为空,本质是用了 Exec 却期望获取输出值。
-
IN参数:直接按顺序传入,和普通 SQL 绑定一致 -
OUT/INOUT参数:MySQL 不支持在客户端直接接收,需改用用户变量(@var_name)中转 - 调用后必须额外执行
SELECT @var_name才能拿到输出值
OUT 参数必须通过用户变量中转
Go 的 database/sql 驱动不解析存储过程的参数模式,它把整个 CALL 当作一条普通 SQL 执行。所以你不能写 CALL proc(?, ?) 并期待第二个 ? 自动映射为 OUT —— 它只会被当成一个未赋值的 NULL 输入。
正确做法是显式使用 MySQL 用户变量:
// 先调用,把 OUT 参数存进 @o_id、@o_name 等
_, err := db.Exec("CALL query_student(?, ?, @o_id, @o_name, @o_age, @o_gender, @o_phone, @o_address)", "Jerry", 1)
if err != nil {
panic(err)
}
// 再单独查这些变量
row := db.QueryRow("SELECT @o_id, @o_name, @o_age, @o_gender, @o_phone, @o_address")
var id int64
var name, phone, address string
var age, gender int
err := row.Scan(&id, &name, &age, &gender, &phone, &address)
注意:@o_id 这类变量作用域是当前连接(connection),不是会话(session)或全局,所以必须在同一个 *sql.DB 连接对象上调用,不能跨 db 实例或复用连接池中不同底层连接。
GORM v2 不原生支持存储过程调用
GORM 的 Session、Raw 和 Exec 方法看起来能跑 CALL,但实际有严重限制:
-
db.Raw("CALL ...").Scan(&v):仅能接收第一个结果集,无法处理多结果集(如存储过程中多个SELECT) -
db.Exec("CALL ..."):完全丢弃所有结果集和用户变量,OUT参数彻底不可达 - 不支持用户变量语法(
@x)在Raw中安全插值,易被 SQL 注入
如果你坚持用 GORM,唯一稳妥方式是退回到原生 *sql.DB:通过 db.Statement.ConnPool.(*sql.DB)(需类型断言)拿到底层连接,再用上面的 Exec + QueryRow 组合操作。
事务内调用存储过程要特别注意 autocommit 行为
MySQL 存储过程默认在自己的事务上下文中运行,但 Go 层开启的事务(tx, _ := db.Begin())不会自动传播到 CALL 内部 —— 除非你在存储过程中显式加 START TRANSACTION 或依赖外部事务隔离级别。
更关键的是:如果存储过程里有 COMMIT 或 ROLLBACK,它会提前结束 Go 层的事务,导致 tx.Commit() 失败并报错 sql: transaction has already been committed or rolled back。
建议做法:
- 存储过程内部避免显式
COMMIT/ROLLBACK,让它作为“原子子操作”嵌入 Go 层事务 - 若必须控制事务,统一收口到 Go 层,存储过程只做 DML,不碰事务控制语句
- 测试时务必用
tx.QueryRow而非db.QueryRow,确保在同一个事务连接上执行
真正容易被忽略的点是连接复用和用户变量生命周期 —— 同一个 *sql.DB 对象可能复用底层连接,而用户变量 @x 在连接关闭后就消失;没显式 defer tx.Rollback() 或异常路径遗漏,会导致后续请求意外读到前一次残留的 @x 值。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











