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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 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,
  },
}


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
16天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
19天前
|
监控 关系型数据库 MySQL
MySQL自增ID耗尽应对策略:技术解决方案全解析
在数据库管理中,MySQL的自增ID(AUTO_INCREMENT)属性为表中的每一行提供了一个唯一的标识符。然而,当自增ID达到其最大值时,如何处理这一情况成为了数据库管理员和开发者必须面对的问题。本文将探讨MySQL自增ID耗尽的原因、影响以及有效的应对策略。
61 3
|
20天前
|
存储 关系型数据库 MySQL
MySQL 字段类型深度解析:VARCHAR(50) 与 VARCHAR(500) 的差异
在MySQL数据库中,`VARCHAR`类型是一种非常灵活的字符串存储类型,它允许存储可变长度的字符串。然而,`VARCHAR(50)`和`VARCHAR(500)`之间的差异不仅仅是长度的不同,它们在存储效率、性能和使用场景上也有所不同。本文将深入探讨这两种字段类型的区别及其对数据库设计的影响。
36 2
|
24天前
|
存储 关系型数据库 MySQL
PHP与MySQL动态网站开发深度解析####
本文作为技术性文章,深入探讨了PHP与MySQL结合在动态网站开发中的应用实践,从环境搭建到具体案例实现,旨在为开发者提供一套详尽的实战指南。不同于常规摘要仅概述内容,本文将以“手把手”的教学方式,引导读者逐步构建一个功能完备的动态网站,涵盖前端用户界面设计、后端逻辑处理及数据库高效管理等关键环节,确保读者能够全面掌握PHP与MySQL在动态网站开发中的精髓。 ####
|
27天前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,并与使用 RPM 包安装进行了对比
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,并与使用 RPM 包安装进行了对比。通过具体案例,读者可以了解如何准备环境、下载源码、编译安装、配置服务及登录 MySQL。编译源码安装虽然复杂,但提供了更高的定制性和灵活性,适用于需要高度定制的场景。
75 3
|
1月前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
87 2
|
1月前
|
存储 关系型数据库 MySQL
MySQL MVCC深度解析:掌握并发控制的艺术
【10月更文挑战第23天】 在数据库领域,MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种重要的并发控制机制,它允许多个事务并发执行而不产生冲突。MySQL作为广泛使用的数据库系统,其InnoDB存储引擎就采用了MVCC来处理事务。本文将深入探讨MySQL中的MVCC机制,帮助你在面试中自信应对相关问题。
100 3
|
1月前
|
缓存 关系型数据库 MySQL
MySQL执行计划深度解析:如何做出最优选择
【10月更文挑战第23天】 在数据库查询性能优化中,执行计划的选择至关重要。MySQL通过查询优化器来生成执行计划,但有时不同的执行计划会导致性能差异。理解如何选择合适的执行计划,以及为什么某些计划更优,对于数据库管理员和开发者来说是一项必备技能。
51 2
|
2月前
|
Java 关系型数据库 MySQL
【编程基础知识】Eclipse连接MySQL 8.0时的JDK版本和驱动问题全解析
本文详细解析了在使用Eclipse连接MySQL 8.0时常见的JDK版本不兼容、驱动类错误和时区设置问题,并提供了清晰的解决方案。通过正确配置JDK版本、选择合适的驱动类和设置时区,确保Java应用能够顺利连接MySQL 8.0。
195 1
|
2月前
|
架构师 关系型数据库 MySQL
MySQL最左前缀优化原则:深入解析与实战应用
【10月更文挑战第12天】在数据库架构设计与优化中,索引的使用是提升查询性能的关键手段之一。其中,MySQL的最左前缀优化原则(Leftmost Prefix Principle)是复合索引(Composite Index)应用中的核心策略。作为资深架构师,深入理解并掌握这一原则,对于平衡数据库性能与维护成本至关重要。本文将详细解读最左前缀优化原则的功能特点、业务场景、优缺点、底层原理,并通过Java示例展示其实现方式。
91 1

推荐镜像

更多