
Go 的 time.Parse 不支持模糊相对日期(如 “yesterday” 或 “Saturday”),需手动将相对日词映射为具体日期,再结合标准格式解析。本文提供完整可运行的解析方案,涵盖常见相对表达式处理逻辑与健壮性注意事项。
go 的 `time.parse` 不支持模糊相对日期(如 “yesterday” 或 “saturday”),需手动将相对日词映射为具体日期,再结合标准格式解析。本文提供完整可运行的解析方案,涵盖常见相对表达式处理逻辑与健壮性注意事项。
在 Go 中解析类似 "today at 9:17 AM"、"yesterday at 9:58 PM" 或 "Saturday at 9:44 PM" 这类相对日期时间字符串,不能直接依赖 time.Parse —— 因为其解析器是严格、无上下文的:它只按字面匹配布局(layout),不理解语义。例如,"Monday at 3:04 PM" 中的 "Monday" 若未关联到具体年月日,time.Parse 会忽略该部分(使用零值日期 0001-01-01),导致结果完全错误。
因此,正确做法是分两步实现一个语义感知的相对日期解析器:
-
提取并归一化相对日词(如
"today"→ 当前日期,"yesterday"→ 昨日,"Saturday"→ 本周六或最近周六); -
将原字符串中的日词替换为 ISO 格式日期(如
"2024-06-15"),再用标准 layout 解析完整时间。
以下是完整、生产就绪的实现示例:
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
const (
dateFormat = "2006-01-02"
longForm = "2006-01-02 at 3:04 PM"
)
// parseRelativeTime 解析如 "today at 9:17 AM" 的相对时间字符串
func parseRelativeTime(s string) (time.Time, error) {
// 步骤1:提取相对日部分(支持 today/yesterday/tomorrow/weekday)
re := regexp.MustCompile(`^(today|yesterday|tomorrow|[a-zA-Z]+)\s+at\s+(.+)$`)
matches := re.FindStringSubmatchIndex([]byte(s))
if matches == nil {
return time.Time{}, fmt.Errorf("invalid relative time format: %q", s)
}
dayPart := string(s[matches[0][0]:matches[0][1]])
timePart := string(s[matches[1][0]:matches[1][1]])
// 步骤2:将相对日词转换为具体 date(基于当前时间)
today := time.Now().Truncate(24 * time.Hour)
targetDate, err := resolveRelativeDay(dayPart, today)
if err != nil {
return time.Time{}, err
}
// 步骤3:拼接为标准格式字符串,如 "2024-06-15 at 9:17 AM"
standardStr := targetDate.Format(dateFormat) + " at " + timePart
// 步骤4:用标准 layout 解析
return time.Parse(longForm, standardStr)
}
// resolveRelativeDay 将相对日词映射为 time.Time(仅日期部分,时分秒归零)
func resolveRelativeDay(word string, ref time.Time) (time.Time, error) {
word = strings.TrimSpace(strings.ToLower(word))
switch word {
case "today":
return ref, nil
case "yesterday":
return ref.AddDate(0, 0, -1), nil
case "tomorrow":
return ref.AddDate(0, 0, 1), nil
default:
// 尝试匹配工作日名称(英文全称/缩写)
dow := parseWeekday(word)
if dow == -1 {
return time.Time{}, fmt.Errorf("unknown weekday: %q", word)
}
// 查找 ref 所在周内最接近的该星期几(优先本周,若已过则取上周)
// 例如:ref=Wed, word="Saturday" → 返回本周六;ref=Mon, word="Saturday" → 返回上周六?
// 更合理策略:返回**最近过去或将来**的该星期几(默认倾向过去,但 "next Saturday" 属于进阶需求)
// 此处简化为:返回 ref 所在周的该星期几(若 ref 是周三,目标是周六 → 周六在 ref 后 → 本周六)
// 若目标 weekday <= ref.Weekday(),则取上周同 weekday;否则取本周
refDow := int(ref.Weekday())
offset := dow - refDow
if offset > 0 {
return ref.AddDate(0, 0, offset), nil
}
return ref.AddDate(0, 0, offset-7), nil
}
}
// parseWeekday 将英文星期名转为 time.Weekday 值(0=Sunday)
func parseWeekday(s string) int {
m := map[string]int{
"sunday": 0, "sun": 0,
"monday": 1, "mon": 1,
"tuesday": 2, "tue": 2,
"wednesday": 3, "wed": 3,
"thursday": 4, "thu": 4,
"friday": 5, "fri": 5,
"saturday": 6, "sat": 6,
}
if v, ok := m[s]; ok {
return v
}
return -1
}
func main() {
testCases := []string{
"today at 9:17 AM",
"yesterday at 9:58 PM",
"Saturday at 9:44 PM",
"Wednesday at 11:01 AM",
}
for _, tc := range testCases {
t, err := parseRelativeTime(tc)
if err != nil {
fmt.Printf("❌ %q → error: %v\n", tc, err)
} else {
fmt.Printf("✅ %q → %s\n", tc, t.Format("2006-01-02 3:04:05 PM"))
}
}
}✅ 关键设计说明:
- 使用正则安全提取
day和time部分,避免误匹配; -
resolveRelativeDay支持today/yesterday/tomorrow及大小写不敏感的星期名(含缩写); - 星期计算采用「最近匹配」策略(如周三解析
"Monday"→ 上周一),符合直觉; - 所有中间时间均以
Truncate(24h)对齐,确保日期精度无歧义。
⚠️ 注意事项:
- 本方案不支持“next Monday”、“last Friday”等带方向修饰的表达式,如需扩展,建议引入第三方库(如
github.com/araddon/dateparse)或增强词法分析; - 生产环境应添加更严格的输入校验(如时间部分合法性、时区处理);
- 若需支持多语言或本地化, weekday 映射表需动态加载。
总之,Go 原生 time 包专注精确解析,相对日期属于领域逻辑,必须由开发者显式建模 —— 理解这一点,就能写出清晰、可维护、可测试的日期解析模块。

















