Go の MySQL ドライバーとして go-sql-driver/mysql を使用した際の挙動にハマったので、解決策を記しておきます。
クエリを発行し、取得した整数型の値をany型の値で受け取る際に予期しない結果となることがあります。
package main
import (
"database/sql"
"fmt"
"os"
"github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", (&mysql.Config{
User: "root",
Passwd: "password",
DBName: "rdb",
Addr: "localhost:3000",
AllowNativePasswords: true,
}).FormatDSN())
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
var val any
if err := db.QueryRow("SELECT 1").Scan(&val); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("type is %T\n", val)
var val2 any
if err := db.QueryRow("SELECT ?", 1).Scan(&val2); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("type is %T\n", val2)
}
go run main.go
type is []uint8
type is int64
2つのSELECT文は同じ値を返しているはずですが、プレースホルダを用いているか否かで、値の型が異なってしまいます。
期待するのは、どちらもint64になることです。
解決策
プレースホルダへのバインドが不要であってもプリペアドステートメントを使うよう実装することができます。
https://github.com/go-sql-driver/mysql/issues/407#issuecomment-172583652
下記が実装例です。
package main
import (
"database/sql"
"fmt"
"os"
"github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", (&mysql.Config{
User: "root",
Passwd: "password",
DBName: "rdb",
Addr: "localhost:3000",
AllowNativePasswords: true,
}).FormatDSN())
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
var val any
stmt, err := db.Prepare("SELECT 1")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer stmt.Close()
if err := stmt.QueryRow().Scan(&val); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("type is %T\n", val)
var val2 any
if err := db.QueryRow("SELECT ?", 1).Scan(&val2); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("type is %T\n", val2)
}
go run main.go
type is int64
type is int64
解説
MySQL にはバイナリプロトコル、テキストプロトコルという2つのプロトコルがあります。
go-sql-driver/mysql は、プリペアドステートメントを使用する際にはバイナリプロトコルを使用し、そうでない場合はテキストプロトコルを使用します。
テキストプロトコルでデータを受信した場合には[]uint8
で結果を取得し、バイナリプロトコルの場合にはカラムの型に応じた Go の型で結果を取得します。
上記を踏まえると、テキストプロトコルが使用されるケースでScan()
にany
型の値を渡した際に[]uint8
型の値がアサインされるのは自然です。
また、テキストプロトコルが使用されるケースであってもint64
型の値を渡せばint64
型の値がアサインされるので、型を強制したいのであればany
型を渡すなよということです。
こちらの記事が詳しいです。
go-sql-driver/mysql の次期バージョンを待ちましょう
今回、解決策を示していますが、実は go-sql-driver/mysql 次期バージョンにおいて上記の問題は解決されます。
テキストプロトコルが使用されるケースであっても、整数、浮動小数点をint64
やfloat64
にパースするPRがマージされています。
Parse numbers on text protocol too by methane · Pull Request #1452 · go-sql-driver/mysql · GitHub
v1.8 のリリースを待ちましょう。
参考