使用go-mssqldb调用存储过程时遇decimal转nvarchar错误排查
让我一步步帮你解决遇到的两个问题:空指针panic和decimal转nvarchar的类型转换错误,以及存储过程调用的参数传递问题。
1. 空指针Panic的直接原因
你当前的代码逻辑存在一个致命问题:当db.QueryContext执行失败返回错误时,rows变量会是nil,但你依然执行了defer rows.Close()和rows.Next()操作,这直接触发了空指针panic。必须先检查错误,确认执行成功后再处理结果集。
2. 存储过程调用的姿势错误
使用database/sql调用SQL Server存储过程时,不能直接把存储过程名传给QueryContext——SQL驱动会把它当成普通SQL语句解析,而不是存储过程调用。正确的做法是用EXEC关键字显式调用,并且要为输出参数加上OUTPUT标记,这样驱动才能正确识别参数的方向和类型。
3. 类型转换错误的根源
错误提示mssql: Error converting data type decimal to nvarchar,说明某个decimal类型的参数被驱动错误识别为nvarchar,或者参数传递的顺序/名称不匹配,导致类型错位(比如把decimal参数传给了nvarchar类型的参数位置)。
具体修复步骤
步骤1:先解决空指针Panic
修改代码逻辑,在调用db.QueryContext或db.ExecContext后,立即检查错误。如果错误存在,直接终止程序,不要继续操作结果集:
// 执行存储过程后立刻检查错误 rows, err := db.QueryContext(ctx, execSql, ...) if err != nil { log.Fatal("Execute failed: ", err.Error()) } // 只有执行成功时,defer Close才有意义 defer rows.Close()
步骤2:修正存储过程调用语句
使用EXEC显式调用存储过程,并为每个参数指定名称(和存储过程定义的参数名一致),同时标记输出参数@TotalTax为OUTPUT:
// 注意参数顺序可以和存储过程定义不同,但名称必须严格匹配 execSql := `EXEC EAPI_CalculateTax @WarehouseID=@WarehouseID, @TaxCode=@TaxCode, @LineItemTotals=@LineItemTotals, @TotalTax=@TotalTax OUTPUT, @ShipToAddress1=@ShipToAddress1, @ShipToAddress2=@ShipToAddress2, @ShipToCity=@ShipToCity, @ShipToState=@ShipToState, @ShipToZip=@ShipToZip, @ISOCountryCode=@ISOCountryCode, @FreightCharge=@FreightCharge, @CustomerID=@CustomerID, @Timeout=@Timeout`
步骤3:确保Decimal类型的正确映射
shopspring/decimal虽然是常用的Go decimal库,但要确保go-mssqldb驱动能正确识别它。另外,你当前初始化TotalTax和FreightCharge时忽略了错误(用_接收),这可能导致无效的decimal值,进而引发类型转换问题。建议改为:
TotalTax, err := decimal.NewFromString("00.00") if err != nil { log.Fatal("Invalid TotalTax value: ", err.Error()) } FreightCharge, err := decimal.NewFromString("12.10") if err != nil { log.Fatal("Invalid FreightCharge value: ", err.Error()) }
如果驱动依然无法识别该类型,可以尝试将decimal转换为字符串后传递(因为SQL Server的decimal类型可以接收合法的数字字符串)。
步骤4:选择合适的执行方法
如果你的存储过程仅返回输出参数而没有结果集,建议使用db.ExecContext替代db.QueryContext——QueryContext是为查询结果集设计的,无结果集时会返回空的rows,增加出错风险:
// 使用ExecContext执行存储过程(适合无结果集的场景) _, err = db.ExecContext(ctx, execSql, sql.Named("WarehouseID", WarehouseID), sql.Named("TaxCode", TaxCode), sql.Named("LineItemTotals", LineItemTotals), sql.Named("ShipToAddress1", ShipToAddress1), sql.Named("ShipToAddress2", ShipToAddress2), sql.Named("ShipToCity", ShipToCity), sql.Named("ShipToState", ShipToState), sql.Named("ShipToZip", ShipToZip), sql.Named("ISOCountryCode", ISOCountryCode), sql.Named("FreightCharge", FreightCharge), sql.Named("CustomerID", CustomerID), sql.Named("Timeout", Timeout), sql.Named("TotalTax", sql.Out{Dest: &TotalTax}), ) if err != nil { log.Fatal("Execute stored procedure failed: ", err.Error()) } // 直接使用输出参数TotalTax即可 fmt.Printf("TotalTax is %s", TotalTax.String())
完整修复后的代码
package main import ( "context" "database/sql" "flag" "fmt" _ "github.com/denisenkom/go-mssqldb" "github.com/shopspring/decimal" "log" ) var ( debug = flag.Bool("debug", true, "enable debugging") userid = flag.String("U", "user", "the database user") password = flag.String("P", "password", "the database password") server = flag.String("S", "address", "the database server") port *int = flag.Int("port", 1433, "the database port") database = flag.String("d", "database", "the database") encrypt = flag.String("e", "disable", "the encryption flag") ) func main() { flag.Parse() if *debug { fmt.Printf(" port:%d\n", *port) fmt.Printf(" server:%s\n", *server) fmt.Printf(" user:%s\n", *userid) fmt.Printf(" database:%s\n", *database) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() connString := fmt.Sprintf("server=%s;port=%d;user id=%s;password=%s;database=%s;encrypt=%s;", *server, *port, *userid, *password, *database, *encrypt) if *debug { fmt.Printf(" connString:%s\n", connString) } db, err := sql.Open("sqlserver", connString) if err != nil { log.Fatal("Open connection failed:", err.Error()) } defer db.Close() // bigint WarehouseID := 1 // nvarchar(7) TaxCode := "Vertex" // nvarchar(max) LineItemTotals := "16.35" // decimal(19,5) - 初始化时不要忽略错误 TotalTax, err := decimal.NewFromString("00.00") if err != nil { log.Fatal("Invalid TotalTax: ", err) } // nvarchar(50) ShipToAddress1 := "Address1" // nvarchar(50) ShipToAddress2 := "Address2" // nvarchar(40) ShipToCity := "Seattle" // nvarchar(5) ShipToState := "WA" // nvarchar(10) ShipToZip := "zip" // nchar(3) ISOCountryCode := "USA" // decimal(19,5) - 初始化时不要忽略错误 FreightCharge, err := decimal.NewFromString("12.10") if err != nil { log.Fatal("Invalid FreightCharge: ", err) } // bigInt CustomerID := 456118 // bigint Timeout := 5000 // 显式的EXEC调用语句,标记输出参数为OUTPUT execSql := `EXEC EAPI_CalculateTax @WarehouseID=@WarehouseID, @TaxCode=@TaxCode, @LineItemTotals=@LineItemTotals, @TotalTax=@TotalTax OUTPUT, @ShipToAddress1=@ShipToAddress1, @ShipToAddress2=@ShipToAddress2, @ShipToCity=@ShipToCity, @ShipToState=@ShipToState, @ShipToZip=@ShipToZip, @ISOCountryCode=@ISOCountryCode, @FreightCharge=@FreightCharge, @CustomerID=@CustomerID, @Timeout=@Timeout` // 使用ExecContext执行存储过程 _, err = db.ExecContext(ctx, execSql, sql.Named("WarehouseID", WarehouseID), sql.Named("TaxCode", TaxCode), sql.Named("LineItemTotals", LineItemTotals), sql.Named("ShipToAddress1", ShipToAddress1), sql.Named("ShipToAddress2", ShipToAddress2), sql.Named("ShipToCity", ShipToCity), sql.Named("ShipToState", ShipToState), sql.Named("ShipToZip", ShipToZip), sql.Named("ISOCountryCode", ISOCountryCode), sql.Named("FreightCharge", FreightCharge), sql.Named("CustomerID", CustomerID), sql.Named("Timeout", Timeout), sql.Named("TotalTax", sql.Out{Dest: &TotalTax}), ) if err != nil { log.Fatal("Execute stored procedure failed: ", err.Error()) } fmt.Printf("TotalTax is %s", TotalTax.String()) }
内容的提问来源于stack exchange,提问作者ozfive

