404 Not Found

404 Not Found


nginx

正则与日期

第24课:正则与日期

生活类比引入

想象你是一个快递分拣员:

在编程中,正则帮你在文本中精准定位和提取信息,而 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 CompileMustCompile 在编译失败时直接 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.Parsetime.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 TimeDuration 格式化、解析、计算、定时器

正则要点

时间要点


📝 作业

练习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:敏感词过滤系统

实现一个敏感词过滤器:

  1. 从配置加载敏感词列表,编译为正则
  2. 支持 * 号替换和完全删除两种模式
  3. 支持敏感词变体检测(如中间插入空格:赌 博
GO
// 输入文本:"这是一个赌博网站,提供赌 博服务"
// 替换模式输出:"这是一个*网站,提供*服务"
// 删除模式输出:"这是一个网站,提供服务"

下一课

下一课我们将学习 命令行程序开发,了解如何使用 Go 构建强大的 CLI 工具,包括参数解析、子命令、交互式输入等实用技能。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏