Go 时间格式化为什么用 2006-01-02:time.Format/Parse 的参考时间、时区与解析踩坑
2026/9/5 9:37:30 网站建设 项目流程

Go 时间格式化为什么用 2006-01-02:time.Format/Parse 的参考时间、时区与解析踩坑

第一次用 Go 格式化时间的人几乎都会愣一下:别的语言里格式化字符串是yyyy-MM-dd HH:mm:ss这种占位符,到了 Go 变成了2006-01-02 15:04:05。你甚至会怀疑是不是抄错了——为什么是这么一串看起来像随机日期的数字?

这不是随机的,而且一旦你搞错一个数字,格式化出来的结果会静默地不对,编译器一句警告都不给。这篇文章把 Go 时间格式化的设计、正确写法,以及最容易翻车的时区和解析问题一次讲清。

那串魔数到底是什么

Go 没有用yyyyMM这类抽象占位符,而是用一个具体的参考时间当模板。这个参考时间是固定的:

2006-01-02 15:04:05

把它按「1 2 3 4 5 6 7」的顺序念一遍就懂了:

  • 01→ 月(Month,第 1)
  • 02→ 日(Day,第 2)
  • 15→ 时(Hour,第 3,24 小时制,注意是 15 不是 03)
  • 04→ 分(Minute,第 4)
  • 05→ 秒(Second,第 5)
  • 2006→ 年(Year,第 6)
  • -07→ 时区偏移(Zone,第 7)

也就是这个参考时间正好是01/02 03:04:05 PM '06 -0700(美式记法 1、2、3、4、5、6、7),Go 团队用它当助记。你要什么格式,就用这个参考时间「摆」成那个样子。想要2024/03/08,模板就写2006/01/02;想要08-03-2024,就写02-01-2006

packagemainimport("fmt""time")funcmain(){t:=time.Date(2024,3,8,15,4,5,0,time.Local)fmt.Println(t.Format("2006-01-02 15:04:05"))// 2024-03-08 15:04:05fmt.Println(t.Format("2006/01/02"))// 2024/03/08fmt.Println(t.Format("15:04"))// 15:04fmt.Println(t.Format("2006年01月02日"))// 2024年03月08日}

关键就一句:模板里出现哪个魔数,对应位置就填哪个字段;魔数以外的字符(横杠、斜杠、汉字)原样输出。

最高频的翻车:小时用错、月和分搞反

新手最容易踩的两个坑,编译器完全不报错,结果却是错的。

坑一:24 小时制要用 15,不是 03。参考时间里15代表 24 小时制的小时,03代表 12 小时制。如果你想要 24 小时制却写了03:

t:=time.Date(2024,3,8,20,30,0,0,time.Local)// 晚上 8 点半fmt.Println(t.Format("2006-01-02 03:04:05"))// 2024-03-08 08:30:00 ← 20 点被输出成 08!fmt.Println(t.Format("2006-01-02 15:04:05"))// 2024-03-08 20:30:00 ← 正确

03(12 小时制)时通常要配PM才有意义:03:04:05 PM。想要不带前导零的 12 小时制用3

坑二:月是 01,分是 04,别搞反。01是月、04是分。写04-02想表示「月-日」是错的,那会输出「分-日」:

t:=time.Date(2024,3,8,15,45,0,0,time.Local)fmt.Println(t.Format("01-02"))// 03-08 月-日,正确fmt.Println(t.Format("04-02"))// 45-08 分-日,几乎肯定不是你要的

记牢那句口诀「1 月 2 日 3 时 4 分 5 秒 6 年」,月在时之前(1、2、3),分在时之后(4)。

常用的格式占位速查

除了主力的那几个,还有些常用变体:

t:=time.Date(2024,3,8,9,4,5,123456789,time.Local)t.Format("2006-1-2")// 2024-3-8 不带前导零(1、2 而非 01、02)t.Format("Jan 2, 2006")// Mar 8, 2024 月份英文缩写t.Format("January")// March 月份英文全名t.Format("Mon")// Fri 星期缩写t.Format("Monday")// Friday 星期全名t.Format("15:04:05.000")// 09:04:05.123 毫秒(补零到 3 位)t.Format("15:04:05.999")// 09:04:05.123 毫秒(尾部零省略)t.Format("2006-01-02T15:04:05Z07:00")// RFC3339 带时区

标准库还预定义了一批常量,能用就别手写:

t.Format(time.RFC3339)// 2024-03-08T09:04:05+08:00t.Format(time.DateTime)// 2024-03-08 09:04:05 (Go 1.20+)t.Format(time.DateOnly)// 2024-03-08 (Go 1.20+)t.Format(time.TimeOnly)// 09:04:05 (Go 1.20+)

time.DateTime/DateOnly/TimeOnly是 Go 1.20 加的,正好对应最常用的三种格式,直接用比手敲2006-01-02 15:04:05更不容易打错。

解析:time.Parse 和它的时区陷阱

time.Parse(layout, value)Format的逆操作,layout 用同一套魔数描述 value 的格式:

t,err:=time.Parse("2006-01-02 15:04:05","2024-03-08 15:04:05")iferr!=nil{panic(err)}fmt.Println(t)// 2024-03-08 15:04:05 +0000 UTC

注意输出末尾的+0000 UTC——这是第一个大坑:如果 layout 里没有时区信息,time.Parse会把这个时间当成 UTC,而不是你本地时区。上面字符串是「2024-03-08 15:04:05」,你以为是北京时间下午 3 点,Go 却理解成 UTC 的下午 3 点,差了 8 小时。

如果这个时间其实是本地时间,要用time.ParseInLocation显式指定时区:

loc,_:=time.LoadLocation("Asia/Shanghai")t,_:=time.ParseInLocation("2006-01-02 15:04:05","2024-03-08 15:04:05",loc)fmt.Println(t)// 2024-03-08 15:04:05 +0800 CSTfmt.Println(t.UTC())// 2024-03-08 07:04:05 +0000 UTC 正确换算

这个坑在处理用户输入、数据库里的「无时区时间戳」时极其常见。规则记死:字符串里不带时区偏移,就必须用ParseInLocation告诉 Go 它是哪个时区的时间,否则一律被当 UTC。

第二个坑:layout 和 value 的格式必须严格对齐,包括分隔符和位数。差一个字符就报错:

_,err:=time.Parse("2006-01-02","2024/03/08")fmt.Println(err)// parsing time "2024/03/08" as "2006-01-02": cannot parse "/03/08" ..._,err=time.Parse("2006-01-02 15:04:05","2024-03-08")// value 缺了时间部分fmt.Println(err)// ... cannot parse "" as "15"

所以拿到一个时间字符串,先看清它到底长什么样,layout 一字不差地照着摆。

时区加载的一个部署坑

time.LoadLocation("Asia/Shanghai")依赖系统的时区数据库(通常在/usr/share/zoneinfo)。在精简的容器镜像里(比如scratchalpine没装 tzdata),这个调用会失败:

loc,err:=time.LoadLocation("Asia/Shanghai")iferr!=nil{// alpine 上常见:unknown time zone Asia/Shanghailog.Fatal(err)}

两种解法。一是镜像里装上时区数据(alpineapk add tzdata);二是让 Go 自带一份——从 Go 1.15 起,匿名导入time/tzdata就会把时区库编译进二进制,不再依赖系统文件:

import_"time/tzdata"// 时区数据打进二进制,容器里也能 LoadLocation

代价是二进制体积增加几百 KB,但换来「到处都能加载时区」的确定性,对容器化部署很值。

一个能直接用的小工具

把常见需求封装一下,顺手贴一个「解析本地时间字符串并转成 UTC 存储」的函数,这是后端存时间的推荐姿势(存 UTC、展示时再转本地):

// ParseLocalToUTC 把「无时区的本地时间字符串」解析成 UTC 时间。// 例:输入 "2024-03-08 15:04:05" + "Asia/Shanghai",返回对应的 UTC time.Time。funcParseLocalToUTC(s,zonestring)(time.Time,error){loc,err:=time.LoadLocation(zone)iferr!=nil{returntime.Time{},err}// 用 ParseInLocation 避免被当成 UTC,再转 UTC 落库t,err:=time.ParseInLocation(time.DateTime,s,loc)iferr!=nil{returntime.Time{},err}returnt.UTC(),nil}

存 UTC 的好处是:不管服务器部署在哪个时区、用户在哪个时区,底层时间点是唯一确定的,展示时再t.In(loc).Format(...)转回去即可,不会出现「跨时区各显示各的」这种脏数据。

小结

  • Go 用参考时间2006-01-02 15:04:05当格式模板,按 1 月 2 日 3 时 4 分 5 秒 6 年记:月=01、日=02、时(24 制)=15、分=04、秒=05、年=2006。
  • 两个静默错误:24 小时制要用15(用03会把 20 点显示成 08),月是01、分是04,别搞反。
  • 优先用标准库常量time.DateTime/DateOnly/TimeOnly/RFC3339,少手敲少出错。
  • time.Parse遇到不带时区的字符串会当成 UTC;要按本地时区解析必须用time.ParseInLocation并传*time.Location
  • 精简容器里LoadLocation会因缺 tzdata 失败,匿名导入_ "time/tzdata"把时区库编进二进制最省心。

一句话记忆:格式化认「1234567」口诀,解析认「不带时区就是 UTC」这条铁律,存时间一律存 UTC、展示再转本地。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询