直接用database/sql+mysql驱动即可实现gin原生sql操作,关键在于避免连接泄漏、参数绑定错误和错误处理缺失;需sql.open后立即db.ping()验证连通性,设置连接池参数,使用?占位符,scan时字段须导出并正确处理null及time.time。

直接用 database/sql + github.com/go-sql-driver/mysql 就能跑通 Gin 的原生 SQL 操作,不需要 GORM 或其他 ORM 层。关键不是“能不能整合”,而是你是否绕开了常见连接泄漏、参数绑定错误和错误处理缺失这三类坑。
怎么初始化一个安全可用的 *sql.DB 实例
很多人直接在 handler 里调用 sql.Open,这是错的 —— 它不真正建连,只是返回一个连接池对象;更糟的是,如果没设 SetMaxOpenConns 和 SetMaxIdleConns,高并发下会拖垮 MySQL。
-
sql.Open后必须立刻执行db.Ping()验证连通性,否则首次查询失败才暴露问题 - 连接字符串里的
parseTime=true和loc=Local必须加,否则time.Time字段会解析成零值或 panic - 建议把
*sql.DB作为全局变量或注入到 handler,而不是每次请求都新建 - 别 defer
db.Close()在初始化函数里 —— 它关的是整个池,不是单次连接
query / exec 时参数占位符必须用 ?,不能用 $1 或 :name
MySQL 驱动只认 ? 占位符,写成 或 :id 会直接报错 sql: expected 0 arguments, got 1 或更迷惑的 driver: bad connection(其实是语法解析失败后连接被丢弃)。
- 所有参数按顺序填进
Exec或Query的可变参数列表,比如db.Exec("INSERT INTO user(name, age) VALUES(?, ?)", name, age) - 批量插入不要拼 SQL 字符串,用
VALUES (?, ?), (?, ?), ...然后传入展开的参数切片(需手动展开,Go 不支持直接传 slice) - WHERE IN 查询不能直接写
IN (?),得动态生成IN (?,?,?)再传对应数量参数
Scan 时 struct 字段必须导出且 tag 匹配列名
用 rows.Scan 或 db.QueryRow 时,如果目标 struct 字段是小写(如 userName string),哪怕加了 json:"user_name" tag,也无法赋值 —— database/sql 只看字段是否导出(首字母大写)和 column tag(不是 json)。
- 正确写法:
UserName string `column:"user_name"`或保持数据库列名与字段名一致(如User_name string) - 如果列名含下划线,又不想改结构体字段名,必须显式指定
columntag,否则 Scan 会跳过该字段 - 注意 NULL 值:MySQL 的
NULL无法直接 Scan 到普通类型,得用sql.NullString、sql.NullInt64等包装类型
事务里别混用 db.Query 和 tx.Query
一旦开始事务(tx, _ := db.Begin()),后续所有操作必须用 tx 对象,不能穿插使用原始 db。否则事务失效,且可能因连接复用导致数据错乱。
- 事务内所有
Query、Exec、QueryRow都要调tx.Xxx() - 记得显式
tx.Commit()或tx.Rollback(),别依赖 defer —— 错误分支容易漏 rollback - 事务中若嵌套调用其他函数,要把
*sql.Tx作为参数传进去,而不是在函数里重新取db
最常被忽略的其实是连接池配置和 NULL 处理 —— 很多“查不到数据”或“接口偶发超时”,根源都在 db.SetMaxOpenConns(20) 没设,或者某个 varchar 列允许 NULL 却用了普通 string 接收。











