在用golang进行后端开发时,总免不了要和mysql打交道。
我们一般使用库github.com/go-sql-driver/mysql作为mysql driver。这篇文章主要阐述初学者在使用mysql driver时容易犯的几个错误及其解决方案
1 时间戳解析
如何将mysql中的timestamp转化为golang中的time.Time结构,这是一个问题...
1.1 问题
首先在mysql上创建表结构并插入数据
CREATE TABLE `test` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=136 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; insert into test (create_time) values (now());
golang代码如下
package main import ( "log" "net" "strings" "time" "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx/reflectx" "github.com/k0kubun/pp" ) type TestData struct { Id int `json:"id"` CreateTime time.Time `json:"create_time"` } func main() { c := mysql.NewConfig() c.DBName = "test" c.Net = "tcp" c.User = "user" c.Passwd = "password" c.Addr = net.JoinHostPort("localhost", "3306") c.Params = map[string]string{} db, err := sqlx.Open("mysql", c.FormatDSN()) if err != nil { log.Fatal(err) } db.Mapper = reflectx.NewMapperFunc("json", strings.ToLower) res := []TestData{} err = db.Select(&res, "select id, create_time from test.test") if err != nil { log.Fatal(err) } pp.Println(res) }
运行结果
$ go run main.go 2021/07/19 18:00:00 sql: Scan error on column index 1, name "create_time": unsupported Scan, storing driver.Value type []uint8 into type *time.Time exit status 1
1.2 解决
在Config.Params中加入"parseTime"开关
c.Params = map[string]string{}
变成
c.Params = map[string]string{ "parseTime": "true", }
再次运行,我们就可以读到mysql中的值了
[]main.TestData{ main.TestData{ Id: 136, CreateTime: 2021-07-19 17:52:08 UTC, }, }
1.3 原理
为什么增加一个"parseTime"开关就好使了呢?我们分析下mysql driver库的代码
1.3.0 三个依赖库的关系
从代码中可可知,我们的代码依赖三个库,这三个库都与mysql相关。
database/sql github.com/jmoiron/sqlx github.com/go-sql-driver/mysql
其中database/sql是golang内置库,它约定了一系列访问支持SQL的数据库的接口,其中并不包含实现。
github.com/go-sql-driver/mysql是mysql driver, 它实现了database/sql库中的一系列接口。因此只需要将mysql driver中的实现注册到database/sql中,即可通过
database/sql中的接口访问clickhouse.
github.com/jmoiron/sqlx是对database/sql中一系列接口的封装和扩展,使得用户更方便使用.
1.3.1 mysql driver的注册和使用
在mysql driver的初始化函数中,将MySQLDriver
注册到了database/sql中
func init() { sql.Register("mysql", &MySQLDriver{}) }
1.3.1.1 如何注册mysql driver
sql.Register的实现如下,这里将key=mysql, value=MySQLDriver加入到了sql.drivers中
func Register(name string, driver driver.Driver) { driversMu.Lock() defer driversMu.Unlock() if driver == nil { panic("sql: Register driver is nil") } if _, dup := drivers[name]; dup { panic("sql: Register called twice for driver " + name) } drivers[name] = driver }
而sql.driver的数据结构如下, 即MysqlDriver需要实现Driver中的方法
var ( driversMu sync.RWMutex drivers = make(map[string]driver.Driver) ) type Driver interface { Open(name string) (Conn, error) } type Conn interface { Prepare(query string) (Stmt, error) Close() error Begin() (Tx, error) }
1.3.1.2 如何使用mysql driver
到此为止,mysql driver已经注册到了database/sql中,那么database/sql如何调用呢?下面的代码中,
- 首先从sql.drivers中根据driverName(对于mysql driver来说,就是mysql)获取对应的sql.Driver实例,
- 然后将该实例转化为sql.driver.DriverContext类型(mysql driver中也实现了sql.driver.DriverContext接口)
- 接着调用driverCtx.OpenConnector返回sql.driver.Connector实例(底层类型是mysql.connector, 它实现了sql.driver.Connector实例)
- 最后将connector传给OpenDB, 返回sql.DB实例
func Open(driverName, dataSourceName string) (*DB, error) { driversMu.RLock() driveri, ok := drivers[driverName] driversMu.RUnlock() if !ok { return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName) } if driverCtx, ok := driveri.(driver.DriverContext); ok { connector, err := driverCtx.OpenConnector(dataSourceName) if err != nil { return nil, err } return OpenDB(connector), nil } return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil }
其实dsnConnector实现了sql.driver.Connector接口,如何实现以及实现中如何调用mysql driver实例留待读者分析
1.3.1 mysql database的打开
在我们的应用代码中,调用了sqlx.Open
db, err := sqlx.Open("mysql", c.FormatDSN())
而sqlx.Open中调用了sql.Open, 返回sqlx.DB对象,其中封装了sql.DB对象(来自于database/sql)
func Open(driverName, dataSourceName string) (*DB, error) { db, err := sql.Open(driverName, dataSourceName) if err != nil { return nil, err } return &DB{DB: db, driverName: driverName, Mapper: mapper()}, err }
sql.Open的实现见1.3.1.2
1.3.2 mysql database的查询
在应用代码中, 执行对应的sql语句,结果放置于res中
err = db.Select(&res, "select id, create_time from test.test")
应用代码调用了sqlx.Select, 实现如下:
func Select(q Queryer, dest interface{}, query string, args ...interface{}) error { rows, err := q.Queryx(query, args...) if err != nil { return err } // if something happens here, we want to make sure the rows are Closed defer rows.Close() return scanAll(rows, dest, false) }
因此,查询整体上分为两步
- 查询mysql database中,获取rows数据
- 将rows数据反序列化到res中
1.3.2.1 从mysql database中获取rows数据
在1.3.2 sqlx.Select函数中,q即为从应用传入的db,db类型为sqlx.DB 因此rows, err := q.Queryx(query, args...)
调用了函数sqlx.DB.Queryx
func (db *DB) Queryx(query string, args ...interface{}) (*Rows, error) { r, err := db.DB.Query(query, args...) if err != nil { return nil, err } return &Rows{Rows: r, unsafe: db.unsafe, Mapper: db.Mapper}, err }
而sqlx.DB.Queryx之后的调用链如下:
sql.DB.Query sql.DB.QueryContext sql.DB.query sql.DB.queryDC sql.ctxDriverQuery sql.driver.Queryer.Query
sql.driver.Queryer是interface, 底层实际调用的是mysql.mysqlConn.Query -> mysql.mysqlConn.query:
- 向mysql database发送查询命令
- 读取数据,放置于textRows结构中
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) { if mc.closed.IsSet() { errLog.Print(ErrInvalidConn) return nil, driver.ErrBadConn } if len(args) != 0 { if !mc.cfg.InterpolateParams { return nil, driver.ErrSkip } // try client-side prepare to reduce roundtrip prepared, err := mc.interpolateParams(query, args) if err != nil { return nil, err } query = prepared } // Send command err := mc.writeCommandPacketStr(comQuery, query) if err == nil { // Read Result var resLen int resLen, err = mc.readResultSetHeaderPacket() if err == nil { rows := new(textRows) rows.mc = mc if resLen == 0 { rows.rs.done = true switch err := rows.NextResultSet(); err { case nil, io.EOF: return rows, nil default: return nil, err } } // Columns rows.rs.columns, err = mc.readColumns(resLen) return rows, err } } return nil, mc.markBadConn(err) }
1.3.2.2 将rows数据反序列化到res中
到此为止,我们已经获得了rows数据, 类型为sqlx.Rows,结构中嵌套者sql.Rows结构
type Rows struct { *sql.Rows unsafe bool Mapper *reflectx.Mapper // these fields cache memory use for a rows during iteration w/ structScan started bool fields [][]int values []interface{} }
而sql.Rows结构中嵌套着driver.Rows interface, 在mysql driver中对应的底层结构为textRows
scanAll实现了将sqlx.Rows反序列化到数组中的逻辑, 其中调用了sqlx.Rows.Next 接下来的调用链:
sql.Rows.nextLocked textRows.Next textRows.readRow
1.3.2.3 问题原因
截取mysql.textRows.readRow中部分代码如下 对于时间戳字段来说,mysql返回的是形同yyyy-mm-dd HH:MM:SS
的[]byte
如果开关parseTime的值缺省,默认值为false, 则直接将[]byte赋值给dest[i]
for i := range dest { // Read bytes and convert to string dest[i], isNull, n, err = readLengthEncodedString(data[pos:]) pos += n if err == nil { if !isNull { if !mc.parseTime { continue } else { switch rows.rs.columns[i].fieldType { case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeDate, fieldTypeNewDate: dest[i], err = parseDateTime( dest[i].([]byte), mc.cfg.Loc, ) if err == nil { continue } default: continue } } } else { dest[i] = nil continue } } return err // err != nil }
后续执行sql.convertAssignRows将[]byte转化为time.Time的时候,会返回错误
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest)
而如果开关parseTime的值设置为true, 则mysql.textRows.readRow中会将[]byte转化为time.Time类型,后续执行sql.convertAssignRows时,才能成功注入time.Time类型的值
case time.Time: switch d := dest.(type) { case *time.Time: *d = s return nil
2 时区设置
2.1 问题
细心的同学可能会发现, 即使加上了parseTime = true的开关,main.go输出的时间戳还是有点问题:时区是UTC,不太对
[]main.TestData{ main.TestData{ Id: 136, CreateTime: 2021-07-19 17:52:08 UTC, }, }
2.2 解决
前面我们已经完整的走读了一遍代码。遇到时区问题,我们第一时间想到可能是mysql.textRows.readRow中将[]byte转化为time.Time出了问题
dest[i], err = parseDateTime( dest[i].([]byte), mc.cfg.Loc, )
我们看到,执行parseDateTime解析[]byte的时候,会指定一个参数代表时区,而这个参数来自mc.cfg.Loc, 即main.go中的mysql config,因此解决方案很简单,只需在mysql config中加入本地时区信息即可
c.Loc, _ = time.LoadLocation("Local")
重新运行main.go即可得到正确的时区
[]main.TestData{ main.TestData{ Id: 136, CreateTime: 2021-07-19 17:52:08 Local, }, }