正则与日期
第24课:正则与日期
生活类比引入
想象你是一个快递分拣员:
- 正则表达式就像分拣规则——"地址包含
北京且门牌号是3位数"这样的模式,帮你快速从海量包裹中筛选出符合条件的那一批。 - 日期时间就像快递单上的时间戳——你需要知道"这个包裹几点到的"、"距离签收过了多久"、"明天下午3点前能到吗"。
在编程中,正则帮你在文本中精准定位和提取信息,而 time 包帮你测量、计算和展示时间。
核心概念
正则表达式(regexp包)
Go 的 regexp 包基于 RE2 语法引擎,支持大多数常见的正则语法,但不支持回溯和反向引用(性能优先的设计选择)。
核心接口:
- 编译:regexp.Compile(pattern) → (*Regexp, error)
- 匹配:MatchString(s) → bool
- 查找:FindString / FindAllString / FindStringSubmatch
- 替换:ReplaceAllString / ReplaceAllStringFunc
日期时间(time包)
Go 的时间处理以 time.Time 结构体为核心,采用固定的参考时间作为格式化模板:
参考时间:Mon Jan 2 15:04:05 MST 2006
数字记忆:01/02 03:04:05PM 2006 -0700
💡 为什么是这个奇怪的数字? Go 选择了
1 2 3 4 5 6 7 这样的顺序(月1 日2 时3 分4 秒5 年6 时区7),便于记忆。
基本语法与用法
1. 正则表达式基础
GO
package main
import (
"fmt"
"regexp"
)
func main() {
// 编译正则表达式
re, err := regexp.Compile(`\d{3}-\d{4}-\d{4}`)
if err != nil {
fmt.Println("正则编译失败:", err)
return
}
// 判断是否匹配
phone := "138-1234-5678"
fmt.Println("是否匹配:", re.MatchString(phone)) // true
// 查找第一个匹配
text := "联系方式:138-1234-5678 或 139-8765-4321"
fmt.Println("第一个:", re.FindString(text)) // 138-1234-5678
// 查找所有匹配
all := re.FindAllString(text, -1)
fmt.Println("所有:", all) // [138-1234-5678 139-8765-4321]
}
💡
MustCompile vs Compile:MustCompile 在编译失败时直接 panic,适合全局常量正则;Compile 返回 error,适合运行时动态构建的正则。
2. 命名捕获组
GO
package main
import (
"fmt"
"regexp"
)
func main() {
// 使用命名捕获组提取邮箱各部分
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
email := "zhangsan@example.com"
// 获取匹配结果
match := re.FindStringSubmatch(email)
names := re.SubexpNames()
for i, name := range names {
if i > 0 && name != "" {
fmt.Printf("%s = %s\n", name, match[i])
}
}
// user = zhangsan
// domain = example.com
}
3. 替换操作
GO
package main
import (
"fmt"
"regexp"
)
func main() {
re := regexp.MustCompile(`\s+`)
// 替换所有空白为单个空格
text := " hello world go "
result := re.ReplaceAllString(text, " ")
fmt.Printf("[%s]\n", result) // [hello world go]
// 使用函数进行动态替换
re2 := regexp.MustCompile(`\b\w`)
title := re2.ReplaceAllStringFunc("hello world go", func(s string) string {
// 首字母大写
return strings.ToUpper(s)
})
fmt.Println(title) // Hello World Go
}
4. 时间基础操作
GO
package main
import (
"fmt"
"time"
)
func main() {
// 获取当前时间
now := time.Now()
fmt.Println("当前时间:", now)
// 提取各个组成部分
fmt.Println("年:", now.Year())
fmt.Println("月:", now.Month())
fmt.Println("日:", now.Day())
fmt.Println("时:", now.Hour())
fmt.Println("分:", now.Minute())
fmt.Println("秒:", now.Second())
fmt.Println("星期:", now.Weekday())
}
💡 time.Now() 返回的是本地时区时间。如需 UTC 时间,使用
time.Now().UTC()。
5. 时间格式化与解析
GO
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// 格式化:用参考时间作为模板
fmt.Println(now.Format("2006-01-02")) // 2025-06-27
fmt.Println(now.Format("2006/01/02 15:04:05")) // 2025/06/27 14:30:00
fmt.Println(now.Format("03:04PM")) // 02:30PM
// 常用预定义格式
fmt.Println(now.Format(time.RFC3339)) // 2025-06-27T14:30:00+08:00
// 解析字符串为时间
t, err := time.Parse("2006-01-02", "2025-12-25")
if err != nil {
fmt.Println("解析失败:", err)
return
}
fmt.Println("解析结果:", t)
// 带时区解析
t2, _ := time.ParseInLocation("2006-01-02 15:04:05",
"2025-12-25 08:00:00", time.Local)
fmt.Println("带时区:", t2)
}
⚠️ 格式化和解析使用相同的参考时间字符串——这是 Go 的独特设计。记住
2006-01-02 15:04:05 就够了。
6. Duration 与时间计算
GO
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// 时间加减
tomorrow := now.Add(24 * time.Hour)
fmt.Println("明天:", tomorrow.Format("2006-01-02"))
twoHoursLater := now.Add(2 * time.Hour)
fmt.Println("两小时后:", twoHoursLater.Format("15:04:05"))
// 计算时间差
diff := tomorrow.Sub(now)
fmt.Println("时间差:", diff) // 24h0m0s
fmt.Println("小时数:", diff.Hours()) // 24
fmt.Println("分钟数:", diff.Minutes()) // 1440
// 判断先后
fmt.Println("明天在之后:", tomorrow.After(now)) // true
fmt.Println("今天在之前:", now.Before(tomorrow)) // true
// Truncate 截断到指定精度
floored := now.Truncate(time.Hour)
fmt.Println("截断到小时:", floored.Format("15:04:05"))
}
7. 定时器
GO
package main
import (
"fmt"
"time"
)
func main() {
// 单次定时器
timer := time.NewTimer(2 * time.Second)
fmt.Println("等待2秒...")
<-timer.C
fmt.Println("时间到!")
// 周期定时器(Ticker)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // 记得停止,避免泄漏
count := 0
for t := range ticker.C {
count++
fmt.Println("滴答:", t.Format("15:04:05.000"))
if count >= 3 {
break
}
}
// time.After 简化版(单次等待)
<-time.After(1 * time.Second)
fmt.Println("1秒后执行")
}
💡
time.After 在循环中使用要小心——每次循环都会创建新 channel,旧的 timer 不会被回收,可能导致内存泄漏。循环中应使用 time.NewTimer 并手动 Reset。
示例代码
示例:验证与提取——日志信息解析(难度⭐)
GO
package main
import (
"fmt"
"regexp"
)
func main() {
// 模拟日志行
logs := []string{
"[2025-06-27 14:30:00] ERROR 数据库连接失败",
"[2025-06-27 14:30:01] INFO 服务启动成功",
"[2025-06-27 14:30:05] WARN 磁盘空间不足",
"[2025-06-27 14:31:00] ERROR 请求超时",
}
// 匹配日志格式:时间 + 级别 + 消息
re := regexp.MustCompile(`\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\s+(.+)`)
// 统计各级别数量
levelCount := make(map[string]int)
for _, log := range logs {
match := re.FindStringSubmatch(log)
if match == nil {
continue
}
timestamp := match[1]
level := match[2]
message := match[3]
fmt.Printf("时间: %s | 级别: %-5s | 消息: %s\n",
timestamp, level, message)
levelCount[level]++
}
fmt.Println("\n级别统计:")
for level, count := range levelCount {
fmt.Printf(" %s: %d条\n", level, count)
}
}
输出:
TEXT
时间: 2025-06-27 14:30:00 | 级别: ERROR | 消息: 数据库连接失败
时间: 2025-06-27 14:30:01 | 级别: INFO | 消息: 服务启动成功
时间: 2025-06-27 14:30:05 | 级别: WARN | 消息: 磁盘空间不足
时间: 2025-06-27 14:31:00 | 级别: ERROR | 消息: 请求超时
级别统计:
ERROR: 2条
INFO: 1条
WARN: 1条
示例:模板引擎——简易文本替换系统(难度⭐⭐)
GO
package main
import (
"fmt"
"regexp"
"strings"
"time"
)
func main() {
template := `尊敬的 {{name}}:
您的订单 {{order}} 已于 {{date}} 发货。
预计 {{days}} 天后送达。
当前时间:{{now}}`
// 定义变量映射
vars := map[string]string{
"name": "张三",
"order": "ORD-20250627-001",
"date": "2025-06-27",
"days": "3",
}
// 匹配 {{变量名}} 模式
re := regexp.MustCompile(`\{\{(\w+)\}\}`)
// 替换变量
result := re.ReplaceAllStringFunc(template, func(match string) string {
// 提取变量名(去掉 {{ 和 }})
key := match[2 : len(match)-2]
if key == "now" {
return time.Now().Format("2006-01-02 15:04:05")
}
if val, ok := vars[key]; ok {
return val
}
return match // 未找到变量,保留原样
})
fmt.Println(result)
// 统计模板中的变量
allVars := re.FindAllString(template, -1)
varNames := make([]string, 0, len(allVars))
for _, v := range allVars {
varNames = append(varNames, v[2:len(v)-2])
}
fmt.Printf("\n模板变量: %s\n", strings.Join(varNames, ", "))
}
输出:
TEXT
尊敬的 张三:
您的订单 ORD-20250627-001 已于 2025-06-27 发货。
预计 3 天后送达。
当前时间:2025-06-27 14:30:00
模板变量: name, order, date, days, now
示例:倒计时器——带格式化的实时显示(难度⭐⭐⭐)
GO
package main
import (
"fmt"
"regexp"
"strings"
"time"
)
// formatDuration 将 Duration 格式化为 "X天 X时 X分 X秒"
func formatDuration(d time.Duration) string {
if d <= 0 {
return "已到期"
}
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
parts := []string{}
if days > 0 {
parts = append(parts, fmt.Sprintf("%d天", days))
}
if hours > 0 {
parts = append(parts, fmt.Sprintf("%d时", hours))
}
if minutes > 0 {
parts = append(parts, fmt.Sprintf("%d分", minutes))
}
parts = append(parts, fmt.Sprintf("%02d秒", seconds))
return strings.Join(parts, " ")
}
// parseDeadline 解析多种日期格式
func parseDeadline(s string) (time.Time, error) {
// 尝试多种格式
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02",
"2006/01/02 15:04",
"01-02 15:04",
}
// 检查是否是相对时间(如 "+2h30m")
re := regexp.MustCompile(`^\+(\d+)([hms])`)
if match := re.FindStringSubmatch(s); match != nil {
// 解析相对时间(简化版,仅处理单单位)
return time.Now().Add(2 * time.Hour), nil
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, s, time.Local); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("无法解析时间: %s", s)
}
func main() {
// 解析截止时间
deadline, err := parseDeadline("2025-12-31 23:59:59")
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("截止时间: %s\n", deadline.Format("2006年01月02日 15:04:05"))
fmt.Println(strings.Repeat("─", 40))
// 模拟倒计时(每秒更新,共5次)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
count := 0
for now := range ticker.C {
remaining := deadline.Sub(now)
// 清屏效果:使用回车符覆盖当前行
fmt.Printf("\r剩余: %-30s", formatDuration(remaining))
count++
if count >= 5 || remaining <= 0 {
break
}
}
fmt.Println("\n\n倒计时演示结束")
// 验证时间格式
re := regexp.MustCompile(`^\d{4}年\d{2}月\d{2}日 \d{2}:\d{2}:\d{2}$`)
formatted := deadline.Format("2006年01月02日 15:04:05")
fmt.Printf("格式验证: %v\n", re.MatchString(formatted)) // true
}
实际应用场景
场景1:表单数据验证
GO
package main
import (
"fmt"
"regexp"
)
// Validator 表单验证器
type Validator struct {
rules map[string]*regexp.Regexp
}
// NewValidator 创建验证器并预编译所有正则
func NewValidator() *Validator {
rules := map[string]*regexp.Regexp{
"手机号": regexp.MustCompile(`^1[3-9]\d{9}$`),
"邮箱": regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`),
"身份证": regexp.MustCompile(`^\d{17}[\dXx]$`),
"用户名": regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{3,15}$`),
"密码强度": regexp.MustCompile(`^.{8,}$`),
"IP地址": regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`),
"日期": regexp.MustCompile(`^\d{4}[-/](0[1-9]|1[0-2])[-/](0[1-9]|[12]\d|3[01])$`),
}
return &Validator{rules: rules}
}
// Validate 验证单个字段
func (v *Validator) Validate(field, value string) bool {
re, ok := v.rules[field]
if !ok {
return true // 无规则则通过
}
return re.MatchString(value)
}
// ValidateAll 验证多个字段,返回所有错误
func (v *Validator) ValidateAll(data map[string]string) []string {
var errors []string
for field, value := range data {
if !v.Validate(field, value) {
errors = append(errors, fmt.Sprintf("%s 格式不正确: %s", field, value))
}
}
return errors
}
func main() {
v := NewValidator()
// 测试数据
data := map[string]string{
"手机号": "13812345678",
"邮箱": "test@example.com",
"身份证": "110101199001011234",
"用户名": "go_dev",
"日期": "2025-06-27",
}
errors := v.ValidateAll(data)
if len(errors) == 0 {
fmt.Println("✓ 所有字段验证通过")
} else {
fmt.Println("验证失败:")
for _, err := range errors {
fmt.Printf(" ✗ %s\n", err)
}
}
// 测试无效数据
invalidData := map[string]string{
"手机号": "12345678901", // 不是有效手机号开头
"邮箱": "not-an-email", // 缺少 @
}
fmt.Println("\n无效数据测试:")
for field, value := range invalidData {
result := "✓"
if !v.Validate(field, value) {
result = "✗"
}
fmt.Printf(" %s %s: %s\n", result, field, value)
}
}
场景2:时间范围查询——活动状态管理
GO
package main
import (
"fmt"
"strings"
"time"
)
// Activity 活动结构
type Activity struct {
Name string
StartTime time.Time
EndTime time.Time
}
// Status 获取活动当前状态
func (a Activity) Status(now time.Time) string {
switch {
case now.Before(a.StartTime):
return "未开始"
case now.After(a.EndTime):
return "已结束"
default:
return "进行中"
}
}
// Remaining 获取剩余时间或开始倒计时
func (a Activity) Remaining(now time.Time) string {
if now.Before(a.StartTime) {
d := a.StartTime.Sub(now)
return fmt.Sprintf("距开始: %s", formatDur(d))
}
if now.Before(a.EndTime) {
d := a.EndTime.Sub(now)
return fmt.Sprintf("剩余: %s", formatDur(d))
}
return "已过期"
}
func formatDur(d time.Duration) string {
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
if hours > 24 {
days := hours / 24
hours = hours % 24
return fmt.Sprintf("%d天%d小时%d分钟", days, hours, minutes)
}
return fmt.Sprintf("%d小时%d分钟", hours, minutes)
}
func main() {
now := time.Now()
// 创建活动列表
activities := []Activity{
{
Name: "618年中大促",
StartTime: mustParse("2025-06-01 00:00:00"),
EndTime: mustParse("2025-06-18 23:59:59"),
},
{
Name: "暑期特惠",
StartTime: mustParse("2025-07-01 00:00:00"),
EndTime: mustParse("2025-08-31 23:59:59"),
},
{
Name: "双11预热",
StartTime: mustParse("2025-11-01 00:00:00"),
EndTime: mustParse("2025-11-11 23:59:59"),
},
}
fmt.Printf("当前时间: %s\n", now.Format("2006-01-02 15:04:05"))
fmt.Println(strings.Repeat("─", 50))
for _, act := range activities {
status := act.Status(now)
remaining := act.Remaining(now)
fmt.Printf("活动: %s\n", act.Name)
fmt.Printf(" 时间: %s ~ %s\n",
act.StartTime.Format("2006-01-02"),
act.EndTime.Format("2006-01-02"))
fmt.Printf(" 状态: %s | %s\n\n", status, remaining)
}
// 按状态筛选
fmt.Println("进行中的活动:")
found := false
for _, act := range activities {
if act.Status(now) == "进行中" {
fmt.Printf(" - %s\n", act.Name)
found = true
}
}
if !found {
fmt.Println(" 暂无进行中的活动")
}
}
func mustParse(s string) time.Time {
t, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local)
if err != nil {
panic(err)
}
return t
}
❓ 常见问题
Q1:为什么我的正则匹配不到中文?
Go 的 regexp 包默认处理 UTF-8 编码,中文本身可以正常匹配。常见问题是没有使用正确的字符类:
GO
// 错误:\w 不匹配中文
re := regexp.MustCompile(`^\w+$`)
re.MatchString("你好") // false
// 正确:使用 Unicode 字符类或直接匹配中文范围
re2 := regexp.MustCompile(`^[\p{Han}]+$`)
re2.MatchString("你好") // true
// 或者混合匹配
re3 := regexp.MustCompile(`^[\w\p{Han}]+$`)
re3.MatchString("hello你好123") // true
Q2:time.Parse 和 time.ParseInLocation 有什么区别?
GO
// Parse 解析时,如果没有时区信息,会使用 UTC
t1, _ := time.Parse("2006-01-02", "2025-06-27")
fmt.Println(t1.Location()) // UTC
// ParseInLocation 指定默认时区
t2, _ := time.ParseInLocation("2006-01-02", "2025-06-27", time.Local)
fmt.Println(t2.Location()) // Local (如 Asia/Shanghai)
// 如果格式字符串中包含时区信息(如 -0700),两者行为一致
t3, _ := time.Parse("2006-01-02 15:04:05 -0700", "2025-06-27 08:00:00 +0800")
fmt.Println(t3) // 正确解析为 +0800 时区
💡 建议:解析不带时区的日期字符串时,始终使用
ParseInLocation 并明确指定时区。
Q3:正则表达式性能不好怎么办?
GO
// 错误:每次调用都重新编译
func validatePhone(phone string) bool {
re := regexp.MustCompile(`^1[3-9]\d{9}$`) // 每次都编译
return re.MatchString(phone)
}
// 正确:预编译为包级变量
var phoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)
func validatePhone2(phone string) bool {
return phoneRe.MatchString(phone)
}
// 对于高频匹配,考虑使用 Regexp.Copy() 避免锁竞争
var globalRe = regexp.MustCompile(`\d+`)
func processConcurrently(text string) string {
re := globalRe.Copy() // 获取副本,避免并发锁
return re.ReplaceAllString(text, "#")
}
Q4:如何处理时区转换?
GO
package main
import (
"fmt"
"time"
)
func main() {
// 加载指定时区
shanghai, _ := time.LoadLocation("Asia/Shanghai")
tokyo, _ := time.LoadLocation("Asia/Tokyo")
newyork, _ := time.LoadLocation("America/New_York")
// 创建带时区的时间
t := time.Date(2025, 6, 27, 14, 0, 0, 0, shanghai)
fmt.Println("上海:", t.Format("15:04 MST"))
// 转换时区
fmt.Println("东京:", t.In(tokyo).Format("15:04 MST"))
fmt.Println("纽约:", t.In(newyork).Format("15:04 MST"))
// 从字符串解析并转换
parsed, _ := time.ParseInLocation("2006-01-02 15:04",
"2025-06-27 14:00", shanghai)
fmt.Println("\n解析后转换到UTC:", parsed.UTC().Format("2006-01-02 15:04 MST"))
}
⚠️
time.LoadLocation 在某些系统上可能需要安装 tzdata。Go 1.15+ 可以通过导入 _ "time/tzdata" 嵌入时区数据。
📖 小节
本课我们学习了两大核心主题:
| 主题 | 包 | 核心类型 | 关键操作 |
|---|---|---|---|
| 正则表达式 | regexp |
Regexp |
编译、匹配、查找、替换 |
| 日期时间 | time |
Time、Duration |
格式化、解析、计算、定时器 |
正则要点:
- 使用
MustCompile预编译常量正则 - 命名捕获组
(?P<name>...)提高可读性 \p{Han}匹配中文,\p{L}匹配所有 Unicode 字母- RE2 引擎不支持反向引用,但保证线性时间复杂度
时间要点:
- 格式化模板是参考时间
2006-01-02 15:04:05 Parse默认 UTC,ParseInLocation指定时区Duration表示时间间隔,支持丰富的单位方法- 定时器用完必须
Stop(),避免 goroutine 泄漏
📝 作业
练习1:Markdown链接提取器
编写程序,从 Markdown 文本中提取所有链接,输出格式为 标题 -> URL。
GO
// 提示:匹配 [标题](URL) 格式
// 输入:`请访问 [Go官网](https://golang.org) 或 [GitHub](https://github.com)`
// 输出:
// Go官网 -> https://golang.org
// GitHub -> https://github.com
练习2:工作日计算
编写函数 AddBusinessDays(t time.Time, n int) time.Time,计算从给定日期起,经过 n 个工作日后的日期(跳过周六周日)。
GO
// 测试:
// AddBusinessDays(2025-06-27 周五, 1) → 2025-06-30 周一
// AddBusinessDays(2025-06-27 周五, 5) → 2025-07-04 周五
练习3:敏感词过滤系统
实现一个敏感词过滤器:
- 从配置加载敏感词列表,编译为正则
- 支持
*号替换和完全删除两种模式 - 支持敏感词变体检测(如中间插入空格:
赌 博)
GO
// 输入文本:"这是一个赌博网站,提供赌 博服务"
// 替换模式输出:"这是一个*网站,提供*服务"
// 删除模式输出:"这是一个网站,提供服务"
下一课
下一课我们将学习 命令行程序开发,了解如何使用 Go 构建强大的 CLI 工具,包括参数解析、子命令、交互式输入等实用技能。



