You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 19:55:36