概述

很大一部分gopher都是用过gorm,而time.Time类型也是大家罕用的类型,明天我就次要介绍一个在应用过程中遇到的一个比拟奇怪的问题。

问题景象形容

gorm有一个debug模式,大家并不生疏,开启之后执行的每个sql都能够输入到终端。然而我却遇到了一个奇怪的问题,终端打印的sql显示扫描到0行数据,然而实际上把这个sql拿到数据库中执行是有后果的。

起因假如

遇到这种问题,其实一开始会很莫名其妙,而后可能就会想到,是不是数据库执行的sql基本不是控制台打印进去的sql呢。因为where条件上是有工夫过滤条件的,过后其实曾经有一点狐疑是工夫的问题了,基于这个猜想,去验证并不艰难。

验证环节

因为前面其实我曾经确定了是工夫的问题,咱们这里的验证改为一个简略的场景,不拿我过后的业务数据在这里阐明。我会从新构建一个最简略的场景(这样也不便大家亲自实际),而后捋分明这个过程。

筹备工作

golang环境 我本地是1.18.2 实践上这个没什么影响,不要太低就好
mysql环境(我过后理论业务是用的tidb,为了不便验证,间接应用docker运行一个mysql服务)

代码

package mainimport (    "fmt"    "gorm.io/driver/mysql"    "gorm.io/gorm"    "time")func main() {    // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情    dsn := "root:[email protected](127.0.0.1:3306)/lv_test?charset=utf8mb4&parseTime=True&loc=Local"    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})    if err != nil {        panic(err)    }    db = db.Debug()    var result []*map[string]interface{}    t := time.Now()    fmt.Println(t)    t = t.UTC()    err = db.Table("test").Where("created_at>?", t).Find(&result).Error    if err != nil {        panic(err)    }}

下面的代码是一个最小的验证单元

能够通过上面的命令开启通用日志以及查看日志地位

set global general_log=on;
show variables like 'general_log_file';

执行

执行golang代码

看到输入后果如下,因为工夫被转换成utc工夫,所以输入的sql看起来如同是失常的

2022-09-30 22:00:57.7214462 +0800 CST m=+1.2902701012022/09/30 22:00:57 C:/work/go/code/first/mysql.go:24[2.653ms] [rows:0] SELECT * FROM `test` WHERE created_at>'2022-09-30 14:00:57.721'

然而通过查看 mysql的general log并不是这样

2022-09-30T14:00:57.718992Z        12 Connect   [email protected] on lv_test using TCP/IP2022-09-30T14:00:57.719717Z        12 Query     SET NAMES utf8mb42022-09-30T14:00:57.720416Z        12 Query     SELECT VERSION()2022-09-30T14:00:57.735894Z        12 Prepare   SELECT * FROM `test` WHERE created_at>?2022-09-30T14:00:57.736664Z        12 Execute   SELECT * FROM `test` WHERE created_at>'2022-09-30 22:00:57.7214462'2022-09-30T14:00:57.737528Z        12 Close stmt

能够看到数据库外面实在执行的时候的工夫参数跟gorm打印的并不一样,这样的状况就会给排查问题造成比拟大的困扰。

源码剖析

通过浏览go-sql-driver源码外面的Exec办法,发现调用了interpolateParams办法解决参数(在github.com/go-sql-dirver/mysql目录的connection.go)

case time.Time:            if v.IsZero() {                buf = append(buf, "'0000-00-00'"...)            } else {                buf = append(buf, '\'')                buf, err = appendDateTime(buf, v.In(mc.cfg.Loc))                if err != nil {                    return "", err                }                buf = append(buf, '\'')            }

我简略截取了对传入的time.Time对象的解决办法,发现是把time.Time对象解决成cfg.Loc中的时区,那么这个cfg是哪里来的呢

浏览connector.go,发现是在Connect办法中对mc进行赋值操作

func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {    var err error    // New mysqlConn    mc := &mysqlConn{        maxAllowedPacket: maxPacketSize,        maxWriteSize:     maxPacketSize - 1,        closech:          make(chan struct{}),        cfg:              c.cfg,    }    mc.parseTime = mc.cfg.ParseTime    //.........之后的代码省略}

connector外面的cfg又是哪里来的呢

在driver.go中的Open函数赋值的

func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {    cfg, err := ParseDSN(dsn)    if err != nil {        return nil, err    }    c := &connector{        cfg: cfg,    }    return c.Connect(context.Background())}

这时候咱们就要看ParseDSN中的解决了
通过查看不难找出

// Time Location        case "loc":            if value, err = url.QueryUnescape(value); err != nil {                return            }            cfg.Loc, err = time.LoadLocation(value)            if err != nil {                return            }

通过下面,能够看到,如果传入了loc=Local的参数,入库的工夫会被fomat为东八区的工夫字符串

咱们再看看gorm打印sql时候的解决
gorm源码中的logger目录,sql.go文件

func ExplainSQL(sql string, numericPlaceholder *regexp.Regexp, escaper string, avars ...interface{}) string {    var (        convertParams func(interface{}, int)        vars          = make([]string, len(avars))    )    convertParams = func(v interface{}, idx int) {        switch v := v.(type) {        case bool:            vars[idx] = strconv.FormatBool(v)        case time.Time:            if v.IsZero() {                vars[idx] = escaper + tmFmtZero + escaper            } else {                vars[idx] = escaper + v.Format(tmFmtWithMS) + escaper            }        //......略去前面的代码}

能够看到 在打印日志的时候,工夫是被依照tmFmtWithMS这个格局格式化的
不难找出这个的定义

const (    tmFmtWithMS = "2006-01-02 15:04:05.999"    tmFmtZero   = "0000-00-00 00:00:00"    nullStr     = "NULL")

能够看到,是间接Format的,没有思考时区的状况

总结与论断

golang中的time.Time类型实质上也是由工夫戳和时区形成的,不论是什么时区,工夫戳是一样的,而后依据不同的时区体现出不同的字符串模式的值。

咱们一开始连贯数据库传入的loc=Local参数,驱动就在数据交互操作中把所有的time.Time类型变成本地工夫(东八区),这种状况下utc工夫的2022-09-30 14:00:57.721就变成了东八区的2022-09-30 22:00:57.721。然而gorm在进行日志打印的时候,并没有去思考时区的状况,而是间接打印出了2022-09-30 14:00:57.721这个字符串。这就导致看到的gorm日志和理论执行的sql不统一。其实实际上对数据库来说,即使是datatime类型,其实也是没有时区的概念的,能够认为是一个字符串。