ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

Go 时间格式化为什么用 2006-01-02:time.Format/Parse 的参考时间、时区与解析踩坑 Go 时间格式化为什么用 2006-01-02:time.Format/Parse 的参考时间、时区与解析踩坑第一次用 Go 格式化时间的人几乎都会愣一下:别的语言里格式化字符串是yyyy-MM-dd HH:mm:ss这种占位符,到了 Go 变成了2006-01-02 15:04:05。你甚至会怀疑是不是抄错了——为什么是这么一串看起来像随机日期的数字?这不是随机的,而且一旦你搞错一个数字,格式化出来的结果会静默地不对,编译器一句警告都不给。这篇文章把 Go 时间格式化的设计、正确写法,以及最容易翻车的时区和解析问题一次讲清。那串魔数到底是什么Go 没有用yyyy、MM这类抽象占位符,而是用一个具体的参考时间当模板。这个参考时间是固定的: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(fmttime)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:0508: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 ..._,errtime.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)。在精简的容器镜像里(比如scratch、alpine没装 tzdata),这个调用会失败:loc,err:time.LoadLocation(Asia/Shanghai)iferr!nil{// alpine 上常见:unknown time zone Asia/Shanghailog.Fatal(err)}两种解法。一是镜像里装上时区数据(alpine加apk 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、展示再转本地。
返回列表