在 Go 语言中,时间格式化只要输错一个数字,输出可能完全失控。今天我们来实测:当你在时间格式中传入错误的数字时,会发生什么?

一个线上事故

上周,生产环境出现了一个诡异的 Bug。有用户反馈,系统导出的报表中,日期列显示的是这样的:

2523

开发人员检查代码,发现了这行:

t.Format("206-01-02")

他本想写 2006-01-02,却手误少敲了一个 0。结果,日期和年份被拼接成了 2523……

如果你认为这只是个笑话,那么请继续往下看。

Go 语言时间格式化的"潜规则"

大多数编程语言使用特定占位符,比如:

// Java
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
# Python
datetime.now().strftime("%Y-%m-%d")

但 Go 语言是个异类。它使用了一个参考时间

Mon Jan 2 15:04:05 MST 2006

这个数字串中的每个位置都有特殊含义:

  • 1 = 月份
  • 2 = 日期
  • 15 = 小时(24 小时制)
  • 04 = 分钟
  • 05 = 秒
  • 2006 = 年份

关键规则:Go 的 Format 方法会逐字符扫描格式字符串,遇到数字就替换,遇到非数字就原样输出

实测:输错数字的 5 种典型场景

让我们用代码实测,看看输错不同数字会产生什么结果。测试时间:2023-12-25 14:30:45

场景 1:单个数字 0-9

t := time.Date(2023, 12, 25, 14, 30, 45, 0, time.Local)

fmt.Println(t.Format("0"))  // 输出:0
fmt.Println(t.Format("1"))  // 输出:12
fmt.Println(t.Format("2"))  // 输出:25
fmt.Println(t.Format("3"))  // 输出:2
fmt.Println(t.Format("4"))  // 输出:30
fmt.Println(t.Format("5"))  // 输出:45
fmt.Println(t.Format("6"))  // 输出:6
fmt.Println(t.Format("7"))  // 输出:7
fmt.Println(t.Format("8"))  // 输出:8
fmt.Println(t.Format("9"))  // 输出:9

解读

  • 1-5 有特殊含义,会被替换为时间值
  • 06-9 单独使用时原样输出
  • 特别注意:3 是 12 小时制,所以 14 点 → 2 点
  • 关键6 只有在 06 组合时才是两位年份

场景 2:少写一个 0

fmt.Println(t.Format("206"))   // 输出:2523
// 解析:2→25, 0→0, 6→23
// 拼接:25 + 0 + 23 = 2523

事故原因:本想写 2006,手误写成 206,日期和年份被拼成了 2523

场景 3:数字顺序颠倒

fmt.Println(t.Format("10"))    // 输出:120
// 解析:1→12, 0→0

解读:把 01 写成 10,月份后面多了个 0。

场景 4:连续重复数字

fmt.Println(t.Format("111"))   // 输出:121212
// 解析:1→12, 1→12, 1→12

解读:月份值被重复三次,产生诡异数字。

场景 5:混合输错

fmt.Println(t.Format("12345")) // 输出:122523045
// 解析:1→12, 2→25, 3→2, 4→30, 5→45

解读:月、日、小时、分钟、秒全部拼接,产生了 9 位数。

场景 6:无效数字

fmt.Println(t.Format("789"))   // 输出:789

解读7-9 没有定义,原样输出。

如何避免踩坑?

技巧 1:使用标准库预定义常量

Go 的 time 包已经提供了常用的时间格式常量:

// 常用格式
time.RFC3339      // 2023-12-25T14:30:45+08:00
time.RFC1123      // Mon, 25 Dec 2023 14:30:45 CST
time.DateTime     // 2023-12-25 14:30:45(Go 1.20+)
time.DateOnly     // 2023-12-25(Go 1.20+)
time.TimeOnly     // 14:30:45(Go 1.20+)

// 使用示例
fmt.Println(time.Now().Format(time.DateTime))

好处:无需记忆格式字符串,直接使用标准常量,更安全。

技巧 2:封装自定义格式常量

对于项目中常用的特定格式,可以封装为常量:

// 在项目的 config 或 utils 包中定义
const (
    CNDateFormat   = "2006-01-02"
    CNTimeFormat   = "15:04:05"
    CNDateTimeFormat = "2006-01-02 15:04:05"
)

// 使用
fmt.Println(t.Format(CNDateTimeFormat))

好处:统一管理项目特定格式,便于维护和修改。

一张表记住所有有效数字

数字 含义 示例输出 备注
0 无意义 0 单独使用原样输出
1 月份 12 不带前导零
2 日期 25 不带前导零
3 小时 (12h) 2 14 点→2 点
4 分钟 30
5 45
6-9 无意义 6-9 原样输出

特殊组合

组合 含义 示例输出
01 月份(带前导零) 12
02 日期(带前导零) 25
03 小时(12h 带前导零) 02
04 分钟(带前导零) 30
05 秒(带前导零) 45
06 两位年份(带前导零) 23
15 小时(24h) 14
2006 四位年份 2023

写在最后

Go 语言的时间格式化是一把双刃剑:

  • 优点:直观、易记、人类可读
  • 缺点:容错性差,输错数字会产生诡异输出

核心原则

  1. 只有数字 0-6 是有效占位符
  2. 7-9 会原样输出
  3. 从左到右逐字符解析
  4. 有效数字会被替换为时间值

最佳实践:使用常量或工具函数封装格式字符串,避免手写出错。