golang日常开发系列之三--mysql driver常见问题和源码解析
文章目录
更多精彩内容,请关注微信公众号:后端技术小屋
在用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,
},
}
推荐阅读
更多精彩内容,请扫码关注微信公众号:后端技术小屋。如果觉得文章对你有帮助的话,请多多分享、转发、在看。
文章作者 后端侠
上次更新 2021-07-19