
go时区
时区真的很重要!!!我要彻底的给它弄明白。注意,仅仅就Go而言,其他服务端语言需重新验证
特别注意:datetime是不带分区信息的。timestamp是有分区信息的!!!!!!!!!!!!!!!
🕒 TIMESTAMP vs DATETIME:核心区别
| 特性 | DATETIME | TIMESTAMP |
|---|---|---|
| 存储内容 | 纯时间字符串(无时区) | UTC 时间戳(内部存为 Unix 时间) |
| 是否受时区影响 | ❌ 否,存什么就是什么 | ✅ 是,自动进行时区转换 |
| 范围 | 1000-9999 年 | 1970-2038 年(经典 TIMESTAMP) |
| 存储空间 | 8 字节 | 4 字节(经典 TIMESTAMP) |
| 时区感知 | ❌ 否 | ✅ 是 |
go链接mysql的dsn链接时区起什么作用? (适用datetime类型)
一定要保证数据库时区和dsn的时区要一致!!!
loc 参数的作用:
- 查询时的解析:如何将数据库返回的时间字符串解析为 time.Time
- 写入时的格式化:如何将 time.Time 格式化为 SQL 字符串
举个例子,比如你数据库是东八区的时间。值为2025-09-29 22:28:43,然后你用不同的时区去查询得到的时间则如下所示。
- 2025-09-29 22:28:43 +0800 CST(东八区)
- 2025-09-29 22:28:43 +0000 UTC
意思就是说其实看上去值是一样的,只不过后面的单位不一样。其实也说明了值也不一样,不过我之前以为的是,loc的作用是会自动作 转换的,比如如果你dsn的是东八区,你从数据库查出来的就是和数据库的一样,如果你是UTC,则你查出来的时间就自动转换成了UTC的时间。
但事实证明,不是这个样子的,它仅仅是将你数据库的时间定义成你指定的时间而已。但是这样的话,其实是有很大的问题的,比如如果你的dsn链接和 你的数据库的时区没有保持一致,那么你查出来的时间值都是有问题。
如果是插入的话,则是
time.Time → (按 loc 转成本地时间)→ 格式化为 YYYY-MM-DD HH:MM:SS → 存入 DATETIME
测试表
sql
-- 在数据库中创建一个时间字段
CREATE TABLE test_time (
id INT PRIMARY KEY AUTO_INCREMENT,
event_time DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入一个明确的时间(假设数据库时区是东八区)
INSERT INTO test_time (event_time) VALUES ('2025-09-29 22:28:43');测试代码
go
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"time"
)
type TestTime struct {
ID uint
EventTime time.Time
CreatedAt time.Time
}
func main() {
// 使用 loc=UTC
dsnUTC := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=UTC"
dbUTC, _ := gorm.Open(mysql.Open(dsnUTC), &gorm.Config{})
// 使用 loc=Local (假设本地是东八区)
dsnLocal := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai"
dbLocal, _ := gorm.Open(mysql.Open(dsnLocal), &gorm.Config{})
var result1, result2 TestTime
dbUTC.Table("test_time").First(&result1)
dbLocal.Table("test_time").First(&result2)
fmt.Println("loc=UTC:", result1.EventTime)
fmt.Println("loc=Local:", result2.EventTime)
// 转换为Unix时间戳对比
fmt.Printf("loc=UTC Unix: %d\n", result1.EventTime.Unix())
fmt.Printf("loc=Local Unix: %d\n", result2.EventTime.Unix())
// 时间差
fmt.Printf("时间差: %v\n", result1.EventTime.Sub(result2.EventTime))
}结果
text
loc=UTC: 2025-09-29 22:28:43 +0000 UTC
loc=Local: 2025-09-29 22:28:43 +0800 CST
loc=UTC Unix: 1759184923
loc=Local Unix: 1759156123
时间差: 8h0m0s为什么go代码中全局设置时区没有生效?
原因是 Go 语言的时区处理机制比较特殊。Go 在程序启动时只读取一次时区信息,之后修改 TZ 环境变量是无效的:
问题
go
func main() {
// 程序启动时已经读取了系统时区并缓存
fmt.Println("初始时区:", time.Now().Location()) // Asia/Shanghai
// 这行代码不会生效!
os.Setenv("TZ", "UTC")
fmt.Println("修改TZ后:", time.Now().Location()) // 仍然是 Asia/Shanghai
}解决方案
方案1:在程序启动前设置环境变量
shell
# 在启动程序时设置
TZ=UTC go run main.go方案2:使用固定的时区(推荐)
go
func main() {
// 强制设置全局时区为 UTC
loc, err := time.LoadLocation("UTC")
if err != nil {
panic(err)
}
time.Local = loc
fmt.Println("设置后 time.Now():", time.Now()) // 显示为 UTC 时间
fmt.Println("时区:", time.Now().Location()) // UTC
}方案3:使用特定时区创建时间
go
func main() {
utcLoc, _ := time.LoadLocation("UTC")
shanghaiLoc, _ := time.LoadLocation("Asia/Shanghai")
// 使用特定时区创建时间
nowUTC := time.Now().In(utcLoc)
nowShanghai := time.Now().In(shanghaiLoc)
fmt.Println("UTC时间:", nowUTC)
fmt.Println("上海时间:", nowShanghai)
}插入数据库的时候时区会自动转行么?
场景:比如数据库时区是东八区,dsn也是东八区,gorm插入数据库的时候插入的时间是UTC时间,数据库的值应该是什么?
答案:go会自动转换成东八区的时间,所以数据库的值也是对应的东八区的时间。
也就是说,当你dsn的时间和代码中的Time如果不一样的话,则会自动转换成dsn对应的时间点!!!
go
package main
import (
"fmt"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type TestTime struct {
ID uint
EventTime time.Time
CreatedAt time.Time
}
func main() {
// 使用 loc=UTC
//dsnUTC := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=UTC"
//dbUTC, _ := gorm.Open(mysql.Open(dsnUTC), &gorm.Config{
// Logger: logger.Default.LogMode(logger.Info), // 设置日志级别为Info,显示所有SQL
//})
// 使用 loc=Local (假设本地是东八区)
dsnLocal := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai"
dbLocal, _ := gorm.Open(mysql.Open(dsnLocal), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // 设置日志级别为Info,显示所有SQL
})
// 时间差
fmt.Printf("Local时间: %v\n", time.Now())
utcLoc, _ := time.LoadLocation("UTC")
nowUTC := time.Now().In(utcLoc)
fmt.Printf("UTC时间: %v\n", nowUTC)
dbLocal.Table("test_time").Create(&TestTime{
EventTime: nowUTC,
})
}GORM 的时间处理机制
我来总结一下,也就是说,sql打印的时间都是来源于go的处理,至于如何处理,主要是跟loc有关,如果Time和loc时间分区不一致,就转成loc的时间。 其实很简单!!!
时间转换确实在 Go 中完成
当你在 GORM 中插入数据时,所有的时间转换都在 Go 层面完成,包括:
- 你显式设置的字段(如 event_time)
- 自动生成的字段(如 created_at, updated_at)
created_at 的时间来源
created_at 字段的值取决于:
go
// GORM 内部处理 created_at 的伪代码
func (db *DB) Create(value interface{}) *DB {
// 获取当前时间
now := time.Now() // 关键!这里使用 Go 程序的当前时间
// 如果模型有 CreatedAt 字段,设置时间
if hasCreatedAtField(value) {
setCreatedAtField(value, now)
}
// 在生成 SQL 时,根据 DSN 的 loc 设置进行时区转换
sql := generateSQL(value) // 这里时间会被格式化为字符串
return db
}完整的代码
里面有datetime和TIMESTAMP的示例
go
package main
import (
"fmt"
"testing"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type TestTime struct {
ID uint
EventTime time.Time
CreatedAt time.Time
}
// 使用 loc=UTC
var dsnUTC = "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=UTC"
var dbUTC, _ = gorm.Open(mysql.Open(dsnUTC), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
// 使用 loc=Local (假设本地是东八区)
var dsnLocal = "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai"
var dbLocal, _ = gorm.Open(mysql.Open(dsnLocal), &gorm.Config{Logger: logger.Default.LogMode(logger.Info)})
/*
*
总结,datetime读取的话,对于gorm来说,datetime只是一个字符串而已,至于用什么分区来表示,完全取决于dsn链接的loc
*/
func TestSelectDatetime(t *testing.T) {
var result1, result2 TestTime
dbUTC.Table("test_time").Last(&result1)
dbLocal.Table("test_time").Last(&result2)
fmt.Println("loc=UTC:", result1.EventTime)
fmt.Println("loc=Local:", result2.EventTime)
// 转换为Unix时间戳对比
fmt.Printf("loc=UTC Unix: %d\n", result1.EventTime.Unix())
fmt.Printf("loc=Local Unix: %d\n", result2.EventTime.Unix())
// 时间差
fmt.Printf("时间差: %v\n", result1.EventTime.Sub(result2.EventTime))
}
/*
*
总结,datetime插入的话,time.Time → (按 loc 转成本地时间)→ 格式化为 YYYY-MM-DD HH:MM:SS → 存入 DATETIME
也就是说,gorm在插入之前会先校验你传入的时间是否和loc的分区是否一致,如果不一致,则会帮你转成loc的分区
注意:因为表created_at字段是gorm的保留字段,所以它不会走mysql的默认时间,而是gorm会自动帮你生成,生成的方式是
loc=UTC → GORM 用 UTC 时间 填充 created_at
loc=Asia/Shanghai → GORM 用 CST (UTC+8) 时间填充 created_at
*/
func TestAddDatetime(t *testing.T) {
r1 := TestTime{
EventTime: time.Now(),
}
r2 := TestTime{
EventTime: time.Now(),
}
fmt.Printf("%+v:\n", r1)
fmt.Printf("%+v:\n", r2)
dbUTC.Table("test_time").Create(&r1)
dbLocal.Table("test_time").Create(&r2)
fmt.Println("====================")
fmt.Printf("%+v:\n", r1)
fmt.Printf("%+v:\n", r2)
}
/*
*
总结:timestamp类型是带有分区的,也就是说,其实它只是一个时间戳而已,但是这个时间戳在不同分区中查询的显示的结果不一样。它不像datetime,datetime就是一个字符串而已。
用下面的sql可以很方便的看到变化
-- 设置会话时区为 UTC
SET time_zone = '+00:00';
SELECT * FROM test_time;
-- 设置会话时区为东八区
SET time_zone = '+08:00';
SELECT * FROM test_time;
也就是说,其实上时间戳是一个绝对时间,比如1761793871(2025-10-30 11:11:11),这个时间戳就是指的是UTC的一个时间。
mysql在给gorm返回的时候会根据自己本身服务的默认时区做一次转换,比如mysql的时区是utc,那就转成utc,如果是东八区就转成东八区,
最后将转成好的字符串返回给客户端。客户端再根据loc又自己转了一次!!! 坑太多了!!!
loc=UTC: 2025-10-30 11:11:11 +0000 UTC
loc=Local: 2025-10-30 11:11:11 +0800 CST
*/
func TestSelectTimestamp(t *testing.T) {
var result1, result2 TestTime
dbUTC.Table("test_time").Last(&result1)
dbLocal.Table("test_time").Last(&result2)
fmt.Println("loc=UTC:", result1.CreatedAt)
fmt.Println("loc=Local:", result2.CreatedAt)
}
/*
*
time.Time → (按 loc 转成本地时间)→ 格式化为 YYYY-MM-DD HH:MM:SS → 存入 TIMESTAMP (和datetime一样)
*/
func TestAddTimestamp(t *testing.T) {
loc, _ := time.LoadLocation("UTC")
loc2, _ := time.LoadLocation("Asia/Shanghai")
timeX := time.Unix(int64(1761793871), 0).In(loc)
timeX2 := time.Unix(int64(1761793871), 0).In(loc2)
r1 := TestTime{
EventTime: time.Now(),
CreatedAt: timeX,
}
r2 := TestTime{
EventTime: time.Now(),
CreatedAt: timeX2,
}
fmt.Printf("%+v:\n", r1)
fmt.Printf("%+v:\n", r2)
dbUTC.Table("test_time").Create(&r1)
dbLocal.Table("test_time").Create(&r2)
fmt.Println("====================")
fmt.Printf("%+v:\n", r1)
fmt.Printf("%+v:\n", r2)
}
