sql-mssql驱动支持表值参数(TVP)但默认不启用,需通过实现driver.Valuer接口并配合sql.Named传递命名参数,否则因database/sql标准库不识别TVP而报unsupported type错误。
sql-mssqldb 驱动支持表值参数(TVP),但默认不启用 —— 你必须手动构造 sql.NullInt64 类似的结构体,或更准确地说:用 sql.Named + 自定义类型实现 driver.Valuer 接口,否则会报错 unsupported type for parameter。
sql.Exec 直接传 slice 会失败?Go 的 database/sql 标准库不识别“表值参数”这个概念。它把所有参数都当标量处理,而 TVP 是 SQL Server 特有的复合类型。直接传 []map[string]interface{} 或 [][]interface{},驱动层根本不知道该把它序列化成什么 —— 最终抛出 unsupported type 或静默转为空值。
panic: sql: converting argument $1 type: unsupported type []map[string]interface {}, a slice of map
go-mssqldb 不像 C# 的 SqlParameter 那样有 SqlDbType.Structured 和 TypeName 属性可配置driver.Valuer,告诉驱动:“这是 TVP,按 dbo.MyTableType 格式序列化”driver.Valuer 构造 TVP 参数?核心是写一个结构体,实现 Value() 方法,返回 (driver.Value, error),其中 driver.Value 是一个 [][]interface{} 切片,且第一层长度 = 行数,第二层长度 = 列数,顺序与数据库表类型列定义严格一致。
CREATE TYPE dbo.OrderItemTVP AS TABLE (OrderId INT, ProductId INT, Qty INT)
type OrderItemTVP [][]interface{}<p>func (t OrderItemTVP) Value() (driver.Value, error) {return t, nil}</p><p>// 使用时:rows := OrderItemTVP{{1001, 201, 5},{1001, 202, 3},{1002, 201, 1},}_, err := db.Exec("EXEC dbo.usp_InsertOrderItems @items=?", sql.Named("items", rows))
sql.Named("items", rows) 是关键:命名参数让驱动能匹配存储过程签名dbo.OrderItemTVP 定义完全一致,不能多也不能少nil)会被跳过,但整行 nil 可能触发驱动 panic,建议用 sql.NullInt64 等显式表示 NULL即使 Go 端构造正确,SQL Server 端配置不对也会失败,典型表现是存储过程执行成功但没插入数据,或报错 The table type parameter '@items' must have a valid type name。
@items OrderItemTVP READONLY,得写 @items dbo.OrderItemTVP READONLY
READONLY 是强制的,TVP 在存储过程内不可 UPDATE/DELETE,只能 SELECTdbo schema 下(比如 sales.usp_InsertOrderItems),调用时仍需用 dbo.OrderItemTVP,因为表类型是全局的、schema 绑定的dbo.orderitemtvp ≠ dbo.OrderItemTVP,取决于数据库排序规则,建议统一用大写首字母TVP 的真正瓶颈不在 Go 侧构造,而在表类型定义与存储过程逻辑耦合太紧:改一列就得同步改三处(DB 表类型、Go 结构体字段顺序、存储过程 INSERT 字段列表)。上线前务必用真实数据量压测,尤其是 10 万+ 行时,观察 SQL Server 的 tempdb 压力和网络 payload 大小 —— 这些地方比代码写法更容易成为性能断点。