<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          當 go-sql-driver 遇到 mysql timestamp 的離奇 bug

          共 5319字,需瀏覽 11分鐘

           ·

          2022-12-13 10:27

          對于 Go CURD Boy 來說, github.com/go-sql-driver/mysql 這個庫都不會陌生。 或許有些人可能沒太留意,直接就復制粘貼了 import。 比如我們使用 gorm 的時候,如果不加 _ "github.com/go-sql-driver/mysql" 的話,就會報: panic: sql: unknown driver "mysql" (forgotten import?) 基本上 Go 的 CURD 都離不開這個特別重要的庫。

          不過最近在使用 go-sql-driver/mysql 查詢 mysql 的時候,就出現(xiàn)一個很有意思的 bug, 這里分享出來給大家看看。

          Demo 準備

          數(shù)據(jù)庫準備

                
                CREATE?TABLE?`Test1`?(
          ??`id`?int(11)?unsigned?NOT?NULL?AUTO_INCREMENT,
          ??`create_time`?timestamp?NULL?DEFAULT?CURRENT_TIMESTAMP?ON?UPDATE?CURRENT_TIMESTAMP,
          ??PRIMARY?KEY?(`id`)
          )?ENGINE=InnoDB?AUTO_INCREMENT=101?DEFAULT?CHARSET=utf8mb4?COLLATE=utf8mb4_unicode_ci;

          從這個 sql 語句中可以看出來, create_time 是 timestamp 類型,這里要特別留意 timestamp 這個類型。

          現(xiàn)在插入一條數(shù)據(jù),然后查看剛插入的數(shù)據(jù)的值。

                
                insert?into?Test1?values?(1,?'2022-01-01?00:00:00')

          查看下 msyql 當前的時區(qū):

                
                ?show?VARIABLES?like?'%time_zone%';

          結(jié)果是:

          Variable_name Value
          system_time_zone CST
          time_zone +08:00

          接下來使用 mysql unix_timestamp 查看 create_time 的時間戳

                
                SELECT?unix_timestamp(create_time)?from?Test1?where?id?=?1;

          結(jié)果是:

          unix_timestamp(create_time)
          1640966400

          使用 go-sql-driver 讀取 create_time 的值

                
                package?main

          import?(
          ?"database/sql"
          ?"fmt"
          ?_?"github.com/go-sql-driver/mysql"
          ?"time"
          )

          func?main()?{
          ??var?user?=?"user"
          ??var?pwd?=?"password"
          ??var?dbName?=?"dbname"
          ??dsn?:=?fmt.Sprintf("%s:%s@tcp(localhost:3306)/%s?timeout=100s&parseTime=true&interpolateParams=true",?user,?pwd,?dbName)
          ??db,?err?:=?sql.Open("mysql",?dsn)
          ??if?err?!=?nil?{
          ????panic(err)
          ??}
          ??defer?db.Close()

          ??rows,?err?:=?db.Query("select?create_time?from?Test1?limit?1")
          ??if?err?!=?nil?{
          ????panic(err)
          ??}
          ??for?rows.Next()?{
          ????t?:=?time.Time{}
          ????rows.Scan(&t)
          ????fmt.Println(t)
          ????fmt.Println(t.Unix())
          ??}
          }

          我們運行程序會輸出下面的結(jié)果:

                
                2022-01-01?00:00:00?+0000?UTC
          1640995200

          你發(fā)現(xiàn)問題所在了嗎, 可能順著看下來不太直觀,我用一張圖把結(jié)果粘在一塊就看清楚了。

          2420f2a4b1c87cea7f4da0d74b20eb3f.webp


          看圖中紅色箭頭指向的兩個結(jié)果,用 go-sql-driver 讀取的結(jié)果和在 mysql 中用 unix_timestamp 獲取的結(jié)果明顯是不一樣的。

          通過截圖可以看出來,現(xiàn)在 create_time 在數(shù)據(jù)庫的 2022-01-01 00:00:00 是東八區(qū)的時間,也就是北京時間,這個時間對應的時間戳就是 1640966400. 但是 go 程序讀出來的卻不是, go-sql-driver 讀取到 1640995200 是什么呢?其實細心的小伙伴已經(jīng)看出來了,這是 0 時區(qū)的 2022-01-01 00:00:00。

          再直接點,mysql 的 create_time 是 2022-01-01 00:00:00 +008,而讀取到的是 2022-01-01 00:00:00 +000,他倆壓根就不是一個值。

          基本能看出來 bug 是如何發(fā)生的了。下面接著剖析下 go-sql-driver 源碼,看看是哪里導致的結(jié)果的錯誤。

          go-sq-driver 源碼

          就不貼詳細的源碼了,只貼關(guān)鍵的路徑,有興趣的小伙伴可以順著這個路徑去看。

          aecccd851e3a34deee5aaeb9bd05f94e.webp

          我們詳細關(guān)注調(diào)用路徑中紅色的兩個方塊的內(nèi)存中的值。

          db491638fad232b3f30f8bbb681779d7.webp


                
                //?https://github.com/go-sql-driver/mysql/blob/master/packets.go#L788-L798

          func?(rows?*textRows)?readRow(dest?[]driver.Value)?error?{
          ?
          ?//?...?

          ?//?Parse?time?field
          ?switch?rows.rs.columns[i].fieldType?{
          ?case?fieldTypeTimestamp,
          ??fieldTypeDateTime,
          ??fieldTypeDate,
          ??fieldTypeNewDate:
          ??if?dest[i],?err?=?parseDateTime(dest[i].([]byte),?mc.cfg.Loc);?err?!=?nil?{
          ???return?err
          ??}
          ?}
          }
                
                func?parseDateTime(b?[]byte,?loc?*time.Location)?(time.Time,?error)?{
          ?const?base?=?"0000-00-00?00:00:00.000000"
          ?switch?len(b)?{
          ?case?10,?19,?21,?22,?23,?24,?25,?26:?//?up?to?"YYYY-MM-DD?HH:MM:SS.MMMMMM"

          ??year,?err?:=?parseByteYear(b)

          ??month?:=?time.Month(m)

          ??day,?err?:=?parseByte2Digits(b[8],?b[9])

          ??hour,?err?:=?parseByte2Digits(b[11],?b[12])

          ??min,?err?:=?parseByte2Digits(b[14],?b[15])

          ??sec,?err?:=?parseByte2Digits(b[17],?b[18])

          ??//?https://github.com/go-sql-driver/mysql/blob/master/utils.go#L166-L168
          ??if?len(b)?==?19?{
          ???return?time.Date(year,?month,?day,?hour,?min,?sec,?0,?loc),?nil
          ??}
          ?}
          }

          看到這里基本上就能明白,go-sql-driver 把數(shù)據(jù)庫讀出來的 create_time timestamp 值當做一個字符串,然后按照 mysql timestamp 的標準格式 "0000-00-00 00:00:00.000000" 去解析,分別得到 year, month, day, hour, min, sec。最后依賴傳入 time.Location 值,調(diào)用 go 系統(tǒng)庫 time.Date() 再去生成對應的值。

          這里表面看起來沒有問題,其實這里嚴重依賴了傳入的 time.Location。這個 time.Location 是如何得到的呢?

          通過閱讀源碼,可以明顯看出來,是通過解析傳入的 DSN 的 Loc 獲取。

          https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L467-L47441c68b1bfa6daea3a561e36d8732b0ef.webp

          那如果我們傳入的 dsn 串不帶 loc 時,Loc 就是 默認的 UTC 時區(qū)。

          4397c22679cde359c0e371b370aa3b4d.webp


          讀取數(shù)值不一致的原因分析

          再回頭看開頭的程序,我們初始化 go-sql-driver 的 dsn 是 user:password@tcp(localhost:3306)/dbname?timeout=100s&parseTime=true&interpolateParams=true. 該 dsn 里面并不包含 loc 信息,所以 go-sql-driver 使用了默認的 UTC 時區(qū)。然后解析從 mysql 中獲取的 timestamp 字段了,也就用默認的 UTC 時區(qū)去生成 Date,結(jié)果也就錯了。

          其實問題的主要原因就是:go-sql-driver 并沒有按照數(shù)據(jù)庫的時區(qū)去解析 timestamp 字段,而且依賴了開發(fā)者生成的 dsn 傳入的 loc。當開發(fā)者傳入的 loc 和 數(shù)據(jù)庫的 time_zone 不匹配的時候,所有的 timestamp 字段都會解析錯誤。

          有些人可能有疑問,如果 go-sql-driver 為什么不直接使用 mysql 的時區(qū)去解析 timestamp 呢,提了一個 issue,看官方如何回復吧。

          結(jié)論

          所以說,現(xiàn)階段使用 go-sql-driver 的時候一定要特別注意,生成的 dsn 字符串一定要和數(shù)據(jù)的時區(qū)保持一致,不然的話就會導致 timestamp 字段解析錯誤。

          參考文檔

          1. [The DATE, DATETIME, and TIMESTAMP Types] https://dev.mysql.com/doc/refman/8.0/en/datetime.html
          2. [mysql的timestamp會存在時區(qū)問題?] https://juejin.cn/post/7007044908250824741
          3. [is this is go-sql-driver bug] https://github.com/go-sql-driver/mysql/issues/1379


          推薦閱讀


          福利
          我為大家整理了一份 從入門到進階的Go學習資料禮包 ,包含學習建議:入門看什么,進階看什么。 關(guān)注公眾號 「polarisxu」,回復? ebook ?獲取;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。


          瀏覽 131
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操嫩逼电影网 | 天天干一干 | 色老板在线影院一区二区 | 亚洲字幕第一页 | 蜜臀AV在线免费在线播放 |