使用github.com/go-sql-driver/mysql时Rows.Scan与Scanner对象兼容问题
问题:Go中MySQL驱动Rows.Scan与Row.Scan处理Scanner对象的类型不一致
我在Go应用中尝试使用自定义Scanner对象查询数据库,在Microsoft SQL Server中无问题,但使用github.com/go-sql-driver/mysql驱动时,用Rows.Scan结合Scanner对象查询MySQL,所有类型的src参数均为[]uint8,而Row.Scan则能返回对应正确的Go类型。
数据表结构
CREATE TABLE IF NOT EXISTS fruits ( id BIGINT(20) NOT NULL AUTO_INCREMENT, name VARCHAR(45), year SMALLINT, amount INT, price FLOAT(23), PRIMARY KEY (id) ) ENGINE=InnoDB CHARSET=utf8mb4;
代码实现
type Scanner struct { FieldName string } func (s *Scanner) Scan(src any) error { fmt.Printf("FieldName: %s, src: type=%[2]T value=%[2]v\n", s.FieldName, src) return nil } // 准备查询 id_scanner := &Scanner{"Id"} name_scanner := &Scanner{"Name"} year_scanner := &Scanner{"Year"} amount_scanner := &Scanner{"Amount"} price_scanner := &Scanner{"Price"} // 查询单行(行为符合预期) row := db.QueryRow("select * from fruits where id=?", 1) err := row.Scan(id_scanner, name_scanner, year_scanner, amount_scanner, price_scanner)
此时err为nil,输出符合预期:
FieldName: Id, src: type=int64 value=1 FieldName: Name, src: type=[]uint8 value=[97 112 112 108 101] FieldName: Year, src: type=int64 value=1965 FieldName: Amount, src: type=int64 value=123 FieldName: Price, src: type=float32 value=7.55
但使用Rows.Scan时出现异常:
rows, err := db.Query("select * from fruits;") // err为nil defer rows.Close() for rows.Next() { err := rows.Scan(id_scanner, name_scanner, year_scanner, amount_scanner, price_scanner) // err为nil,但所有字段的src均为[]uint8类型,例如Id字段值[49]对应字符串"1" }
环境为mysql:8.0.31 docker镜像,我期望Rows.Scan能和Row.Scan输出一致,请问问题出在哪里?为何在SQL Server中正常但MySQL中异常?
原因与解决方案
核心原因
这是go-sql-driver/mysql驱动的类型转换逻辑差异导致的,和MySQL协议特性有关:
Row.Scan(来自QueryRow):因为查询时带了参数绑定(id=?,参数为int类型),驱动会主动根据上下文推断字段的预期类型,提前将MySQL返回的字符串格式数据转换为对应的Go原生类型(如int64、float32)。Rows.Scan(来自Query):批量查询场景下,驱动默认采用MySQL协议的字符串模式,即所有字段都以字符串形式返回(对应Go的[]uint8)。由于自定义Scanner接口没有提供类型预期信息,驱动无法自动做类型转换,直接返回原始字节数据。
而SQL Server驱动(如github.com/microsoft/go-mssqldb)的协议设计和处理逻辑不同,不管单行还是多行查询,都会直接根据数据表字段类型返回对应Go类型,不会默认使用字符串模式,因此两种Scan行为一致。
解决方案
1. 显式SQL类型转换
在查询语句中用CAST强制指定字段类型,告诉驱动预期的类型,触发自动转换:
SELECT CAST(id AS SIGNED), name, CAST(year AS SIGNED), CAST(amount AS SIGNED), CAST(price AS DECIMAL) FROM fruits;
2. 在Scanner中手动转换
自己处理[]uint8到目标类型的转换,兼容两种Scan场景:
import "strconv" func (s *Scanner) Scan(src any) error { switch val := src.(type) { case []uint8: str := string(val) switch s.FieldName { case "Id", "Year", "Amount": num, err := strconv.ParseInt(str, 10, 64) if err != nil { return err } fmt.Printf("FieldName: %s, value=%d\n", s.FieldName, num) case "Price": f, err := strconv.ParseFloat(str, 32) if err != nil { return err } fmt.Printf("FieldName: %s, value=%.2f\n", s.FieldName, f) case "Name": fmt.Printf("FieldName: %s, value=%s\n", s.FieldName, str) } default: // 处理Row.Scan返回的原生类型 fmt.Printf("FieldName: %s, src: type=%[2]T value=%[2]v\n", s.FieldName, val) } return nil }
3. 修改驱动连接参数(谨慎使用)
在DSN中添加interpolateParams=true,让驱动提前处理参数推断,但该参数存在SQL注入风险,仅在信任所有查询参数的场景下使用。此方法对批量查询的类型推断效果有限,优先推荐前两种方案。
内容的提问来源于stack exchange,提问作者Fung-Chai
相关产品推荐
相关产品推荐

