golang日常开发系列之三--mysql driver常见问题和源码解析

简介: 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,
  },
}


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
人工智能 API 开发者
HarmonyOS Next~鸿蒙应用框架开发实战:Ability Kit与Accessibility Kit深度解析
本书深入解析HarmonyOS应用框架开发,聚焦Ability Kit与Accessibility Kit两大核心组件。Ability Kit通过FA/PA双引擎架构实现跨设备协同,支持分布式能力开发;Accessibility Kit提供无障碍服务构建方案,优化用户体验。内容涵盖设计理念、实践案例、调试优化及未来演进方向,助力开发者打造高效、包容的分布式应用,体现HarmonyOS生态价值。
1061 27
|
人工智能 小程序 前端开发
【一步步开发AI运动小程序】十九、运动识别中如何解析RGBA帧图片?
本文介绍了如何将相机抽取的RGBA帧图像解析为`.jpg`或`.png`格式,适用于体测、赛事等场景。首先讲解了RGBA图像结构,其为一维数组,每四个元素表示一个像素的颜色与透明度值。接着通过`uni.createOffscreenCanvas()`创建离屏画布以减少绘制干扰,并提供代码实现,将RGBA数据逐像素绘制到画布上生成图片。最后说明了为何不直接使用拍照API及图像转换的调用频率建议,强调应先暂存帧数据,运动结束后再进行转换和上传,以优化性能。
|
索引
【Flutter 开发必备】AzListView 组件全解析,打造丝滑索引列表!
在 Flutter 开发中,AzListView 是实现字母索引分类列表的理想选择。它支持 A-Z 快速跳转、悬浮分组标题、自定义 UI 和高效性能,适用于通讯录、城市选择等场景。本文将详细解析 AzListView 的核心参数和实战示例,助你轻松实现流畅的索引列表。
817 7
|
数据可视化 测试技术 API
前后端分离开发:如何高效调试API?有工具 vs 无工具全解析
在前后端分离的开发模式中,API 调试的效率直接影响项目的质量和交付速度。通过本文的对比分析,我们可以看到无工具调试模式虽具备灵活性和代码复用能力,但在操作便利性和团队协作上稍显不足。而传统的外部调试工具带来了可视化、高效协作与扩展性,却可能存在工具切换带来的开发链路断层问题。Apipost-Hepler 融合了两者的优势,让开发者无需离开熟悉的 IDEA 环境,就能享受可视化调试工具的强大功能。
632 5
|
移动开发 前端开发 JavaScript
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。
|
存储 人工智能 程序员
通义灵码AI程序员实战:从零构建Python记账本应用的开发全解析
本文通过开发Python记账本应用的真实案例,展示通义灵码AI程序员2.0的代码生成能力。从需求分析到功能实现、界面升级及测试覆盖,AI程序员展现了需求转化、技术选型、测试驱动和代码可维护性等核心价值。文中详细解析了如何使用Python标准库和tkinter库实现命令行及图形化界面,并生成单元测试用例,确保应用的稳定性和可维护性。尽管AI工具显著提升开发效率,但用户仍需具备编程基础以进行调试和优化。
1205 9
|
10月前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
473 2
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
895 4
Golang语言之管道channel快速入门篇
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
429 4
Golang语言文件操作快速入门篇

推荐镜像

更多