Regex and Date

Lesson 24: Regex and Date

Life Analogy

Imagine you are a package sorting worker:

In programming, regex helps you precisely locate and extract information from text, while the time package helps you measure, calculate, and display time.


Core Concepts

Regular Expressions (regexp Package)

Go's regexp package is based on the RE2 syntax engine, supporting most common regex syntax, but does not support backtracking and backreferences (a performance-first design choice).

Core interfaces:
- Compile: regexp.Compile(pattern) → (*Regexp, error)
- Match: MatchString(s) → bool
- Find: FindString / FindAllString / FindStringSubmatch
- Replace: ReplaceAllString / ReplaceAllStringFunc

Date and Time (time Package)

Go's time handling centers on the time.Time struct, using a fixed reference time as the formatting template:

Reference time: Mon Jan 2 15:04:05 MST 2006
Numeric mnemonic: 01/02 03:04:05PM 2006 -0700
💡 Why these strange numbers? Go chose the sequence 1 2 3 4 5 6 7 (Month 1, Day 2, Hour 3, Minute 4, Second 5, Year 6, Timezone 7) for easy memorization.


Basic Syntax and Usage

1. Regex Basics

GO
package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Compile regular expression
    re, err := regexp.Compile(`\d{3}-\d{4}-\d{4}`)
    if err != nil {
        fmt.Println("Regex compile failed:", err)
        return
    }

    // Check if it matches
    phone := "138-1234-5678"
    fmt.Println("Matches:", re.MatchString(phone)) // true

    // Find first match
    text := "Contact: 138-1234-5678 or 139-8765-4321"
    fmt.Println("First:", re.FindString(text)) // 138-1234-5678

    // Find all matches
    all := re.FindAllString(text, -1)
    fmt.Println("All:", all) // [138-1234-5678 139-8765-4321]
}
💡 MustCompile vs Compile: MustCompile panics directly on compile failure, suitable for global constant regexes; Compile returns an error, suitable for regexes dynamically built at runtime.

2. Named Capture Groups

GO
package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Use named capture groups to extract parts of an email
    re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)

    email := "zhangsan@example.com"

    // Get match results
    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. Replacement Operations

GO
package main

import (
    "fmt"
    "regexp"
    "strings"
)

func main() {
    re := regexp.MustCompile(`\s+`)

    // Replace all whitespace with a single space
    text := "  hello   world   go  "
    result := re.ReplaceAllString(text, " ")
    fmt.Printf("[%s]\n", result) // [hello world go]

    // Use function for dynamic replacement
    re2 := regexp.MustCompile(`\b\w`)
    title := re2.ReplaceAllStringFunc("hello world go", func(s string) string {
        // Capitalize first letter
        return strings.ToUpper(s)
    })
    fmt.Println(title) // Hello World Go
}

4. Basic Time Operations

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // Get current time
    now := time.Now()
    fmt.Println("Current time:", now)

    // Extract individual components
    fmt.Println("Year:", now.Year())
    fmt.Println("Month:", now.Month())
    fmt.Println("Day:", now.Day())
    fmt.Println("Hour:", now.Hour())
    fmt.Println("Minute:", now.Minute())
    fmt.Println("Second:", now.Second())
    fmt.Println("Weekday:", now.Weekday())
}
💡 time.Now() returns local timezone time. For UTC time, use time.Now().UTC().

5. Time Formatting and Parsing

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()

    // Formatting: use reference time as template
    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

    // Common predefined formats
    fmt.Println(now.Format(time.RFC3339))  // 2025-06-27T14:30:00+08:00

    // Parse string to time
    t, err := time.Parse("2006-01-02", "2025-12-25")
    if err != nil {
        fmt.Println("Parse failed:", err)
        return
    }
    fmt.Println("Parse result:", t)

    // Parse with timezone
    t2, _ := time.ParseInLocation("2006-01-02 15:04:05",
        "2025-12-25 08:00:00", time.Local)
    fmt.Println("With timezone:", t2)
}
⚠️ Formatting and parsing use the same reference time string — this is Go's unique design. Just remember 2006-01-02 15:04:05.

6. Duration and Time Calculations

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()

    // Time addition and subtraction
    tomorrow := now.Add(24 * time.Hour)
    fmt.Println("Tomorrow:", tomorrow.Format("2006-01-02"))

    twoHoursLater := now.Add(2 * time.Hour)
    fmt.Println("Two hours later:", twoHoursLater.Format("15:04:05"))

    // Calculate time difference
    diff := tomorrow.Sub(now)
    fmt.Println("Time difference:", diff)           // 24h0m0s
    fmt.Println("Hours:", diff.Hours())             // 24
    fmt.Println("Minutes:", diff.Minutes())         // 1440

    // Compare order
    fmt.Println("Tomorrow is after:", tomorrow.After(now))  // true
    fmt.Println("Today is before:", now.Before(tomorrow))   // true

    // Truncate to specified precision
    floored := now.Truncate(time.Hour)
    fmt.Println("Truncated to hour:", floored.Format("15:04:05"))
}

7. Timers

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // Single-shot timer
    timer := time.NewTimer(2 * time.Second)
    fmt.Println("Waiting 2 seconds...")
    <-timer.C
    fmt.Println("Time's up!")

    // Periodic timer (Ticker)
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop() // Remember to stop to avoid leaks

    count := 0
    for t := range ticker.C {
        count++
        fmt.Println("Tick:", t.Format("15:04:05.000"))
        if count >= 3 {
            break
        }
    }

    // time.After simplified version (single wait)
    <-time.After(1 * time.Second)
    fmt.Println("Executed after 1 second")
}
💡 Be careful using time.After in loops — each iteration creates a new channel, and old timers won't be garbage collected, potentially causing memory leaks. In loops, use time.NewTimer and manually Reset.


Example Code

Example: Validation and Extraction — Log Parsing (Difficulty ⭐)

GO
package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Simulated log lines
    logs := []string{
        "[2025-06-27 14:30:00] ERROR Database connection failed",
        "[2025-06-27 14:30:01] INFO  Service started successfully",
        "[2025-06-27 14:30:05] WARN  Disk space low",
        "[2025-06-27 14:31:00] ERROR Request timeout",
    }

    // Match log format: time + level + message
    re := regexp.MustCompile(`\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\s+(.+)`)

    // Count by level
    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("Time: %s | Level: %-5s | Message: %s\n",
            timestamp, level, message)

        levelCount[level]++
    }

    fmt.Println("\nLevel statistics:")
    for level, count := range levelCount {
        fmt.Printf("  %s: %d entries\n", level, count)
    }
}
▶ Try it Yourself

Output:

TEXT
Time: 2025-06-27 14:30:00 | Level: ERROR | Message: Database connection failed
Time: 2025-06-27 14:30:01 | Level: INFO  | Message: Service started successfully
Time: 2025-06-27 14:30:05 | Level: WARN  | Message: Disk space low
Time: 2025-06-27 14:31:00 | Level: ERROR | Message: Request timeout

Level statistics:
  ERROR: 2 entries
  INFO: 1 entries
  WARN: 1 entries

Example: Template Engine — Simple Text Replacement System (Difficulty ⭐⭐)

GO
package main

import (
    "fmt"
    "regexp"
    "strings"
    "time"
)

func main() {
    template := `Dear {{name}}:
    Your order {{order}} was shipped on {{date}}.
    Expected delivery in {{days}} days.
    Current time: {{now}}`

    // Define variable mapping
    vars := map[string]string{
        "name":  "Alice",
        "order": "ORD-20250627-001",
        "date":  "2025-06-27",
        "days":  "3",
    }

    // Match {{variable_name}} pattern
    re := regexp.MustCompile(`\{\{(\w+)\}\}`)

    // Replace variables
    result := re.ReplaceAllStringFunc(template, func(match string) string {
        // Extract variable name (remove {{ and }})
        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 // Variable not found, keep as-is
    })

    fmt.Println(result)

    // Count variables in template
    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("\nTemplate variables: %s\n", strings.Join(varNames, ", "))
}
▶ Try it Yourself

Output:

TEXT
Dear Alice:
    Your order ORD-20250627-001 was shipped on 2025-06-27.
    Expected delivery in 3 days.
    Current time: 2025-06-27 14:30:00

Template variables: name, order, date, days, now

Example: Countdown Timer — Real-Time Display with Formatting (Difficulty ⭐⭐⭐)

GO
package main

import (
    "fmt"
    "regexp"
    "strings"
    "time"
)

// formatDuration formats a Duration as "Xd Xh Xm Xs"
func formatDuration(d time.Duration) string {
    if d <= 0 {
        return "Expired"
    }

    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("%dd", days))
    }
    if hours > 0 {
        parts = append(parts, fmt.Sprintf("%dh", hours))
    }
    if minutes > 0 {
        parts = append(parts, fmt.Sprintf("%dm", minutes))
    }
    parts = append(parts, fmt.Sprintf("%02ds", seconds))

    return strings.Join(parts, " ")
}

// parseDeadline parses multiple date formats
func parseDeadline(s string) (time.Time, error) {
    // Try multiple formats
    formats := []string{
        "2006-01-02 15:04:05",
        "2006-01-02",
        "2006/01/02 15:04",
        "01-02 15:04",
    }

    // Check if it's a relative time (e.g., "+2h30m")
    re := regexp.MustCompile(`^\+(\d+)([hms])`)
    if match := re.FindStringSubmatch(s); match != nil {
        // Parse relative time (simplified, single unit only)
        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("unable to parse time: %s", s)
}

func main() {
    // Parse deadline
    deadline, err := parseDeadline("2025-12-31 23:59:59")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Printf("Deadline: %s\n", deadline.Format("2006-01-02 15:04:05"))
    fmt.Println(strings.Repeat("─", 40))

    // Simulate countdown (updates every second, 5 times total)
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    count := 0
    for now := range ticker.C {
        remaining := deadline.Sub(now)

        // Screen clearing effect: overwrite current line with carriage return
        fmt.Printf("\rRemaining: %-30s", formatDuration(remaining))

        count++
        if count >= 5 || remaining <= 0 {
            break
        }
    }

    fmt.Println("\n\nCountdown demo complete")

    // Verify time format
    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("Format verification: %v\n", re.MatchString(formatted)) // true
}
▶ Try it Yourself

Practical Application Scenarios

Scenario 1: Form Data Validation

GO
package main

import (
    "fmt"
    "regexp"
)

// Validator form validator
type Validator struct {
    rules map[string]*regexp.Regexp
}

// NewValidator creates a validator and pre-compiles all regexes
func NewValidator() *Validator {
    rules := map[string]*regexp.Regexp{
        "phone":    regexp.MustCompile(`^1[3-9]\d{9}$`),
        "email":    regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`),
        "id_card":  regexp.MustCompile(`^\d{17}[\dXx]$`),
        "username": regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{3,15}$`),
        "password": regexp.MustCompile(`^.{8,}$`),
        "ip":       regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`),
        "date":     regexp.MustCompile(`^\d{4}[-/](0[1-9]|1[0-2])[-/](0[1-9]|[12]\d|3[01])$`),
    }
    return &Validator{rules: rules}
}

// Validate validates a single field
func (v *Validator) Validate(field, value string) bool {
    re, ok := v.rules[field]
    if !ok {
        return true // No rule means pass
    }
    return re.MatchString(value)
}

// ValidateAll validates multiple fields, returns all errors
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 format incorrect: %s", field, value))
        }
    }

    return errors
}

func main() {
    v := NewValidator()

    // Test data
    data := map[string]string{
        "phone":    "13812345678",
        "email":    "test@example.com",
        "id_card":  "110101199001011234",
        "username": "go_dev",
        "date":     "2025-06-27",
    }

    errors := v.ValidateAll(data)
    if len(errors) == 0 {
        fmt.Println("✓ All fields validated successfully")
    } else {
        fmt.Println("Validation failed:")
        for _, err := range errors {
            fmt.Printf("  ✗ %s\n", err)
        }
    }

    // Test invalid data
    invalidData := map[string]string{
        "phone": "12345678901",  // Not a valid phone number prefix
        "email": "not-an-email", // Missing @
    }

    fmt.Println("\nInvalid data test:")
    for field, value := range invalidData {
        result := "✓"
        if !v.Validate(field, value) {
            result = "✗"
        }
        fmt.Printf("  %s %s: %s\n", result, field, value)
    }
}

Scenario 2: Time Range Query — Activity Status Management

GO
package main

import (
    "fmt"
    "strings"
    "time"
)

// Activity struct
type Activity struct {
    Name      string
    StartTime time.Time
    EndTime   time.Time
}

// Status gets the current activity status
func (a Activity) Status(now time.Time) string {
    switch {
    case now.Before(a.StartTime):
        return "Not started"
    case now.After(a.EndTime):
        return "Ended"
    default:
        return "In progress"
    }
}

// Remaining gets remaining time or countdown
func (a Activity) Remaining(now time.Time) string {
    if now.Before(a.StartTime) {
        d := a.StartTime.Sub(now)
        return fmt.Sprintf("Starts in: %s", formatDur(d))
    }
    if now.Before(a.EndTime) {
        d := a.EndTime.Sub(now)
        return fmt.Sprintf("Remaining: %s", formatDur(d))
    }
    return "Expired"
}

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("%dd %dh %dm", days, hours, minutes)
    }
    return fmt.Sprintf("%dh %dm", hours, minutes)
}

func main() {
    now := time.Now()

    // Create activity list
    activities := []Activity{
        {
            Name:      "Summer Sale",
            StartTime: mustParse("2025-06-01 00:00:00"),
            EndTime:   mustParse("2025-06-18 23:59:59"),
        },
        {
            Name:      "Back to School Special",
            StartTime: mustParse("2025-07-01 00:00:00"),
            EndTime:   mustParse("2025-08-31 23:59:59"),
        },
        {
            Name:      "Black Friday Preview",
            StartTime: mustParse("2025-11-01 00:00:00"),
            EndTime:   mustParse("2025-11-11 23:59:59"),
        },
    }

    fmt.Printf("Current time: %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("Activity: %s\n", act.Name)
        fmt.Printf("  Time: %s ~ %s\n",
            act.StartTime.Format("2006-01-02"),
            act.EndTime.Format("2006-01-02"))
        fmt.Printf("  Status: %s | %s\n\n", status, remaining)
    }

    // Filter by status
    fmt.Println("Activities in progress:")
    found := false
    for _, act := range activities {
        if act.Status(now) == "In progress" {
            fmt.Printf("  - %s\n", act.Name)
            found = true
        }
    }
    if !found {
        fmt.Println("  No activities currently in progress")
    }
}

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
}

❓ FAQ

Q1: Why can't my regex match Chinese characters?

Go's regexp package handles UTF-8 encoding by default, so Chinese characters themselves can be matched normally. The common issue is not using the correct character class:

GO
// Wrong: \w doesn't match CJK
re := regexp.MustCompile(`^\w+$`)
re.MatchString("Salut") // false

// Correct: use Unicode character class or match CJK range directly
re2 := regexp.MustCompile(`^[\p{Han}]+$`)
re2.MatchString("Salut") // true

// Or mixed matching
re3 := regexp.MustCompile(`^[\w\p{Han}]+$`)
re3.MatchString("helloSalut123") // true

Q2: What's the difference between time.Parse and time.ParseInLocation?

GO
// Parse uses UTC when no timezone info is present
t1, _ := time.Parse("2006-01-02", "2025-06-27")
fmt.Println(t1.Location()) // UTC

// ParseInLocation specifies a default timezone
t2, _ := time.ParseInLocation("2006-01-02", "2025-06-27", time.Local)
fmt.Println(t2.Location()) // Local (e.g., Asia/Shanghai)

// If the format string contains timezone info (e.g., -0700), both behave the same
t3, _ := time.Parse("2006-01-02 15:04:05 -0700", "2025-06-27 08:00:00 +0800")
fmt.Println(t3) // Correctly parsed as +0800 timezone
💡 Recommendation: When parsing date strings without timezone, always use ParseInLocation and explicitly specify the timezone.

Q3: What if regex performance is poor?

GO
// Wrong: recompile on every call
func validatePhone(phone string) bool {
    re := regexp.MustCompile(`^1[3-9]\d{9}$`) // Compiles every time
    return re.MatchString(phone)
}

// Correct: pre-compile as a package-level variable
var phoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)

func validatePhone2(phone string) bool {
    return phoneRe.MatchString(phone)
}

// For high-frequency matching, consider using Regexp.Copy() to avoid lock contention
var globalRe = regexp.MustCompile(`\d+`)

func processConcurrently(text string) string {
    re := globalRe.Copy() // Get a copy, avoid concurrent locks
    return re.ReplaceAllString(text, "#")
}

Q4: How to handle timezone conversion?

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // Load specific timezones
    shanghai, _ := time.LoadLocation("Asia/Shanghai")
    tokyo, _ := time.LoadLocation("Asia/Tokyo")
    newyork, _ := time.LoadLocation("America/New_York")

    // Create time with timezone
    t := time.Date(2025, 6, 27, 14, 0, 0, 0, shanghai)
    fmt.Println("Shanghai:", t.Format("15:04 MST"))

    // Convert timezone
    fmt.Println("Tokyo:", t.In(tokyo).Format("15:04 MST"))
    fmt.Println("New York:", t.In(newyork).Format("15:04 MST"))

    // Parse from string and convert
    parsed, _ := time.ParseInLocation("2006-01-02 15:04",
        "2025-06-27 14:00", shanghai)
    fmt.Println("\nParsed then converted to UTC:", parsed.UTC().Format("2006-01-02 15:04 MST"))
}
⚠️ time.LoadLocation may require installing tzdata on some systems. Go 1.15+ can embed timezone data by importing _ "time/tzdata".


📖 Summary

This lesson covered two core topics:

Topic Package Core Types Key Operations
Regular Expressions regexp Regexp Compile, Match, Find, Replace
Date and Time time Time, Duration Format, Parse, Calculate, Timer

Regex Key Points:

Time Key Points:


📝 Exercises

Write a program that extracts all links from Markdown text, outputting in the format Title -> URL.

GO
// Hint: match [Title](URL) format
// Input: `Visit [GoSite](https://golang.org) or [GitHub](https://github.com)`
// Output:
//   GoSite -> https://golang.org
//   GitHub -> https://github.com

Exercise 2: Business Day Calculator

Write a function AddBusinessDays(t time.Time, n int) time.Time that calculates the date after n business days from a given date (skipping Saturday and Sunday).

GO
// Tests:
// AddBusinessDays(2025-06-27 Friday, 1) → 2025-06-30 Monday
// AddBusinessDays(2025-06-27 Friday, 5) → 2025-07-04 Friday

Exercise 3: Sensitive Word Filter System

Implement a sensitive word filter:

  1. Load sensitive word list from configuration, compile to regex
  2. Support * replacement and complete removal modes
  3. Support sensitive word variant detection (e.g., spaces inserted in middle: "gam bling")
GO
// Input text: "This is a gambling website, offering gam bling services"
// Replacement mode output: "This is a * website, offering * services"
// Removal mode output: "This is a website, offering services"

Next Lesson

In the next lesson, we will learn about Command-Line Program Development, understanding how to use Go to build powerful CLI tools, including argument parsing, subcommands, interactive input, and other practical skills.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

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

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