Go: Go 字符串与时间处理

最后更新:2026-08-26

字符串和时间是所有程序的两大基础数据类型——Go 标准库在这两个领域提供了完备的工具链,无需第三方依赖。

strings 包有 40+ function,strconv 包处理所有类型转换,time 包覆盖时间的一切操作。这节课你将掌握 Go 字符串和时间处理的全套工具。

1. 你将学到


2. 一个运维工程师的真实故事

(1) 痛点:手动解析时间戳,每行日志都出错

Bob 是运维工程师,他需要从服务器日志中提取时间戳统计 QPS:

"每天 50 万行日志,时间戳格式穿插 2026-07-08T10:00:00Z07/08/2026 10:00:00 AM 两种。我用 Python 的 datetime 写了 20 行,同事看不懂。换 Go 写,结果时间格式字符串查了 10 次文档。"

他写的第一版:

GO
// 坏代码:时间格式硬编码,不灵活
func parseTimestamp(raw string) (time.Time, error) {
    // 忘记 Go 的时间格式是 2006-01-02 15:04:05,不是随便写
    return time.Parse("2006-01-02 15:04:05", raw)
}

同事们常常混淆 Go 的时间参考格式("2006-01-02 15:04:05")。

(2) Go 的解法:标准库全覆盖

GO
// log_parser.go
package main

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

// 清理日志行 + 提取时间戳
func extractTimestamp(logLine string) (time.Time, error) {
    // 1. strings package:TrimSpace 去空白
    line := strings.TrimSpace(logLine)
    if line == "" {
        return time.Time{}, fmt.Errorf("empty log line")
    }

    // 2. strings package:Split 取第一个字段
    parts := strings.Fields(line)
    if len(parts) < 1 {
        return time.Time{}, fmt.Errorf("no fields")
    }

    // 3. strconv / strings:提取时间字段
    rawTime := strings.Trim(parts[0], "[]")

    // 4. time package:解析时间
    formats := []string{
        time.RFC3339,
        "01/02/2006 03:04:05 PM",
        "2006-01-02 15:04:05",
    }
    for _, f := range formats {
        if t, err := time.Parse(f, rawTime); err == nil {
            return t, nil
        }
    }
    return time.Time{}, fmt.Errorf("unrecognized time format: %s", rawTime)
}

func main() {
    lines := []string{
        "2026-07-08T10:00:00Z [INFO] Server started",
        "07/08/2026 10:05:00 AM [ERROR] Connection timeout",
    }

    for _, line := range lines {
        t, err := extractTimestamp(line)
        if err != nil {
            fmt.Printf("Error: %v\n", err)
            continue
        }
        fmt.Printf("Parsed: %s -> %s\n", line, t.Format(time.RFC822))
    }
}

输出:

TEXT 📖 仅展示
Parsed: 2026-07-08T10:00:00Z [INFO] Server started -> 08 Jul 26 10:00 UTC
Parsed: 07/08/2026 10:05:00 AM [ERROR] Connection timeout -> 08 Jul 26 10:05 UTC

(3) 收益:标准库完备性

package 核心能力 替代第三方库
strings 40+ 字符串函数 无需 lodash/underscore
strconv 类型转换 无需手动解析
strings.Builder 高效拼接 bytes.Buffer
time 时间解析/格式化/定时器 moment.js / date-fns
💡 提示: Go 的时间格式化采用"参考时间" Mon Jan 2 15:04:05 MST 2006(01/02/03/04/05/06 正好对应月/日/时/分/秒/年),记住 2006-01-02 15:04:05 就够了。


3. strings 包核心函数

▶ 示例:strings 常用 12 个函数

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "  Hello, Go World!  "

    // 裁切
    fmt.Printf("TrimSpace: [%s]\n", strings.TrimSpace(s))
    fmt.Printf("Trim: [%s]\n", strings.Trim(s, " !"))
    fmt.Printf("TrimPrefix: %s\n", strings.TrimPrefix(s, "  Hello"))

    // 分割与合并
    parts := strings.Split("a,b,c", ",")
    fmt.Printf("Split: %v\n", parts)
    fmt.Printf("Join: %s\n", strings.Join(parts, "-"))

    // 查找
    fmt.Printf("Contains: %v\n", strings.Contains(s, "Go"))
    fmt.Printf("Index: %d\n", strings.Index(s, "Go"))
    fmt.Printf("Count: %d\n", strings.Count(s, "o"))

    // 替换
    fmt.Printf("Replace: %s\n", strings.Replace(s, "o", "0", 1))
    fmt.Printf("ReplaceAll: %s\n", strings.ReplaceAll(s, "o", "0"))

    // 大小写
    fmt.Printf("ToUpper: %s\n", strings.ToUpper(s))
    fmt.Printf("ToLower: %s\n", strings.ToLower(s))

    // 字段拆分(自动处理空白)
    fields := strings.Fields("  hello   go  world  ")
    fmt.Printf("Fields: %v\n", fields)
}
▶ 试一试

输出:

TEXT 📖 仅展示
TrimSpace: [Hello, Go World!]
Trim: [Hello, Go World]
TrimPrefix: , Go World!  
Split: [a b c]
Join: a-b-c
Contains: true
Index: 8
Count: 3
Replace: Hell0, Go World!  
ReplaceAll: Hell0, G0 W0rld!  
ToUpper:   HELLO, GO WORLD!  
ToLower:   hello, go world!  
Fields: [hello go world]

(1) strings 函数速查

分类 函数 作用
裁切 TrimSpace / Trim / TrimPrefix / TrimSuffix 去空白或指定字符
分割 Split / SplitN / Fields 拆分为 slice
合并 Join slice 合并为字符串
查找 Contains / Index / LastIndex / Count 子串检查
替换 Replace / ReplaceAll 子串替换
大小写 ToUpper / ToLower / Title 大小写转换
构造 Builder / Repeat 高效拼接 / 重复字符串

4. strconv 类型转换

(1) 字符串 ↔ 数字

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    // string → int
    n, err := strconv.Atoi("42")
    fmt.Printf("Atoi: %d, err=%v\n", n, err)

    // int → string
    s := strconv.Itoa(42)
    fmt.Printf("Itoa: %s\n", s)

    // ParseInt(带进制和位数)
    v, _ := strconv.ParseInt("FF", 16, 64)
    fmt.Printf("ParseInt(hex): %d\n", v)

    // ParseFloat
    f, _ := strconv.ParseFloat("3.14", 64)
    fmt.Printf("ParseFloat: %f\n", f)

    // FormatInt / FormatFloat
    fmt.Printf("FormatInt(hex): %s\n", strconv.FormatInt(255, 16))
}

▶ 示例:strconv 与类型转换的区别

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    // int → float64(用类型转换)
    var age int = 28
    fAge := float64(age)
    fmt.Printf("类型转换: %f\n", fAge)

    // string → int(用 strconv)
    numStr := "42"
    if num, err := strconv.Atoi(numStr); err == nil {
        fmt.Printf("strconv: %d\n", num)
    }

    // int → string(用 strconv)
    s := strconv.Itoa(42)
    fmt.Printf("Itoa: %s\n", s)

    // 不能用类型转换把 string 转 int(编译错误)
    // n := int(numStr)  ❌ 编译错误
}
▶ 试一试
场景 用类型转换 用 strconv
int ↔ float64 float64(n)
int ↔ string strconv.Itoa / Atoi
string ↔ float64 ParseFloat / FormatFloat
进制数解析 ParseInt("FF", 16, 64)
整数 ↔ uint/int32

5. strings.Builder 高效拼接

(1) 为什么用 Builder?

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 低效方式:每次 + 都创建新字符串
    s1 := ""
    for i := 0; i < 1000; i++ {
        s1 += "a"  // O(n²) 性能
    }

    // 高效方式:Builder 内部缓冲区
    var sb strings.Builder
    for i := 0; i < 1000; i++ {
        sb.WriteString("a")  // O(n) 性能
    }
    s2 := sb.String()

    fmt.Printf("len=%d, equal=%v\n", len(s2), s1 == s2)
}

▶ 示例:Builder 完整用法

GO
package main

import (
    "fmt"
    "strings"
)

func buildCSV(data [][]string) string {
    var sb strings.Builder
    sb.Grow(1024)  // 预分配内存

    for i, row := range data {
        rowStr := strings.Join(row, ",")
        sb.WriteString(rowStr)
        if i < len(data)-1 {
            sb.WriteByte('\n')  // 写入单个字节
        }
    }

    return sb.String()
}

func main() {
    data := [][]string{
        {"Name", "Age", "City"},
        {"Alice", "28", "Shanghai"},
        {"Bob", "32", "Beijing"},
    }

    csv := buildCSV(data)
    fmt.Println(csv)
    fmt.Printf("Length: %d\n", len(csv))
}
▶ 试一试

输出:

TEXT 📖 仅展示
Name,Age,City
Alice,28,Shanghai
Bob,32,Beijing

Length: 52
🔥 易错: + 拼接字符串每次创建新字符串,O(n²) 性能。strings.Builder 内部维护可变缓冲区,O(n) 性能。大量拼接时差距可达 1000 倍。


6. time package:时间处理

(1) time.Now 与时间格式

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // 当前时间
    now := time.Now()
    fmt.Printf("Now: %v\n", now)

    // 常用格式化
    fmt.Printf("RFC3339: %s\n", now.Format(time.RFC3339))
    fmt.Printf("RFC822: %s\n", now.Format(time.RFC822))
    fmt.Printf("自定义: %s\n", now.Format("2006-01-02 15:04:05"))
    fmt.Printf("日期: %s\n", now.Format("2006-01-02"))
    fmt.Printf("时间: %s\n", now.Format("15:04:05"))
}

▶ 示例:时间解析

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // 解析标准格式
    t1, _ := time.Parse(time.RFC3339, "2026-07-08T10:00:00Z")
    fmt.Printf("RFC3339: %v\n", t1)

    // 解析自定义格式(2006-01-02 15:04:05 = 参考时间)
    t2, _ := time.Parse("2006-01-02 15:04:05", "2026-07-08 10:30:00")
    fmt.Printf("Custom: %v\n", t2)

    // 解析带时区
    t3, _ := time.Parse("2006-01-02T15:04:05-07:00", "2026-07-08T10:00:00+08:00")
    fmt.Printf("With TZ: %v\n", t3)

    // 解析月份和日期的英文格式
    t4, _ := time.Parse("January 2, 2006", "July 8, 2026")
    fmt.Printf("English: %v\n", t4)
}
▶ 试一试

输出:

TEXT 📖 仅展示
RFC3339: 2026-07-08 10:00:00 +0000 UTC
Custom: 2026-07-08 10:30:00 +0000 UTC
With TZ: 2026-07-08 10:00:00 +0800 CST
English: 2026-07-08 00:00:00 +0000 UTC

(2) time.Duration 时间计算

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建 Duration
    d1 := 5 * time.Second
    d2 := 100 * time.Millisecond
    d3 := 2*time.Hour + 30*time.Minute

    fmt.Printf("5s = %d ns\n", d1.Nanoseconds())
    fmt.Printf("100ms = %v\n", d2)
    fmt.Printf("2h30m = %v\n", d3)

    // 时间运算
    now := time.Now()
    later := now.Add(2 * time.Hour)
    duration := later.Sub(now)
    fmt.Printf("差值: %v\n", duration)

    // 比较
    fmt.Printf("5s > 100ms? %v\n", d1 > d2)
    fmt.Printf("d1.String(): %s\n", d1)
}

(3) Duration 单位表

常量 含义
time.Nanosecond 1 ns
time.Microsecond 1 µs = 1000 ns
time.Millisecond 1 ms = 1000 µs
time.Second 1 s = 1000 ms
time.Minute 60 s
time.Hour 60 min

7. Timer / Ticker 定时器

(1) time.Timer:单次定时

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    timer := time.NewTimer(2 * time.Second)

    fmt.Println("等待 2 秒...")
    <-timer.C  // 阻塞直到超时
    fmt.Println("时间到!")

    // 也可以用 time.After(更简洁,但不支持 Stop)
    fmt.Println("再等 1 秒...")
    <-time.After(1 * time.Second)
    fmt.Println("完成!")
}

▶ 示例:time.Ticker:周期定时

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(1 * time.Second)
    done := make(chan bool)

    go func() {
        time.Sleep(5 * time.Second)
        done <- true
    }()

    for count := 1; ; count++ {
        select {
        case t := <-ticker.C:
            fmt.Printf("Tick %d at %s\n", count, t.Format("15:04:05"))
        case <-done:
            ticker.Stop()  // 停止 ticker
            fmt.Println("Done!")
            return
        }
    }
}
▶ 试一试

输出:

TEXT 📖 仅展示
Tick 1 at 10:00:01
Tick 2 at 10:00:02
Tick 3 at 10:00:03
Tick 4 at 10:00:04
Tick 5 at 10:00:05
Done!

(2) Timer vs Ticker

特性 Timer Ticker
触发 单次 周期性
停止 timer.Stop() ticker.Stop()
信号通道 .C .C
重置 timer.Reset(d)
常用场景 超时控制 / 延迟执行 心跳 / 定时任务

8. 时区处理

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // 加载时区
    loc, _ := time.LoadLocation("America/New_York")

    // 在指定时区解析
    t := time.Date(2026, 7, 8, 10, 0, 0, 0, loc)
    fmt.Printf("纽约时间: %s\n", t.Format(time.RFC3339))

    // 转换为其他时区
    shanghai := t.In(time.FixedZone("CST", 8*3600))
    fmt.Printf("上海时间: %s\n", shanghai.Format(time.RFC3339))

    // UTC
    utc := t.UTC()
    fmt.Printf("UTC时间: %s\n", utc.Format(time.RFC3339))
}
方式 说明
time.LoadLocation("Asia/Shanghai") IANA 时区名
time.FixedZone("CST", 8*3600) 固定偏移
t.In(loc) 转换为目标时区
t.UTC() 转换为 UTC
t.Local() 转换为本地时区

9. 完整示例:日志时间戳解析器

GO
// log_analyzer.go
package main

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

// LogEntry 日志条目
type LogEntry struct {
    Timestamp time.Time
    Level     string  // INFO / ERROR / WARN
    Message   string
}

// LogParser 日志解析器
type LogParser struct {
    timeFormats []string
}

func NewLogParser() *LogParser {
    return &LogParser{
        timeFormats: []string{
            time.RFC3339,
            "2006-01-02 15:04:05",
            "01/02/2006 03:04:05 PM",
            "2006/01/02 15:04:05",
            "Jan 2 15:04:05",
        },
    }
}

// Parse 解析单行日志
func (p *LogParser) Parse(line string) (LogEntry, error) {
    line = strings.TrimSpace(line)
    if line == "" {
        return LogEntry{}, fmt.Errorf("empty line")
    }

    parts := strings.Fields(line)
    if len(parts) < 3 {
        return LogEntry{}, fmt.Errorf("too few fields")
    }

    // 尝试解析时间戳(支持多个格式)
    var ts time.Time
    var tsLen int

    for i := 0; i < len(parts); i++ {
        candidate := strings.Trim(parts[i], "[]")
        for _, format := range p.timeFormats {
            if t, err := time.Parse(format, candidate); err == nil {
                ts = t
                tsLen = i + 1
                break
            }
        }
        if !ts.IsZero() {
            break
        }
    }

    if ts.IsZero() {
        return LogEntry{}, fmt.Errorf("no timestamp found in: %s", line)
    }

    remaining := parts[tsLen:]
    if len(remaining) < 2 {
        return LogEntry{}, fmt.Errorf("no level/message after timestamp")
    }

    level := strings.Trim(remaining[0], "[]")
    message := strings.Join(remaining[1:], " ")

    return LogEntry{
        Timestamp: ts,
        Level:     level,
        Message:   message,
    }, nil
}

// Analyze 分析日志集合
func (p *LogParser) Analyze(lines []string) {
    var entries []LogEntry
    var errors int

    // 用 strings.Builder 构建错误报告
    var errBuf strings.Builder
    errBuf.Grow(1024)

    for i, line := range lines {
        entry, err := p.Parse(line)
        if err != nil {
            errors++
            errBuf.WriteString(fmt.Sprintf(" 行 %d: %v\n", i+1, err))
            continue
        }
        entries = append(entries, entry)
    }

    // 按时间排序
    sort.Slice(entries, func(i, j int) bool {
        return entries[i].Timestamp.Before(entries[j].Timestamp)
    })

    // 统计
    var infoCount, errorCount, warnCount int
    for _, e := range entries {
        switch e.Level {
        case "INFO":
            infoCount++
        case "ERROR":
            errorCount++
        case "WARN":
            warnCount++
        }
    }

    // 输出报告
    var report strings.Builder
    report.Grow(2048)

    report.WriteString("=== 日志分析报告 ===\n")
    report.WriteString(fmt.Sprintf("总行数: %d\n", len(lines)))
    report.WriteString(fmt.Sprintf("解析成功: %d\n", len(entries)))
    report.WriteString(fmt.Sprintf("解析失败: %d\n", errors))
    report.WriteString(fmt.Sprintf("INFO: %d, ERROR: %d, WARN: %d\n",
        infoCount, errorCount, warnCount))
    report.WriteString(fmt.Sprintf("时间范围: %s ~ %s\n",
        entries[0].Timestamp.Format(time.RFC3339),
        entries[len(entries)-1].Timestamp.Format(time.RFC3339)))

    if errors > 0 {
        report.WriteString("\n=== 错误详情 ===\n")
        report.WriteString(errBuf.String())
    }

    fmt.Print(report.String())
}

func main() {
    logLines := []string{
        "2026-07-08T10:00:00Z [INFO] Server started",
        "2026-07-08T10:01:15Z [ERROR] Connection timeout to db",
        "2026-07-08T10:02:30Z [WARN] Memory usage 85%",
        "2026-07-08T10:03:45Z [INFO] Request processed in 120ms",
        "invalid line without timestamp",
        "2026-07-08T10:05:00Z [ERROR] Disk space low",
    }

    parser := NewLogParser()
    parser.Analyze(logLines)
}

预期输出:

TEXT 📖 仅展示
=== 日志分析报告 ===
总行数: 6
解析成功: 5
解析失败: 1
INFO: 2, ERROR: 2, WARN: 1
时间范围: 2026-07-08T10:00:00Z ~ 2026-07-08T10:05:00Z

=== 错误详情 ===
 行 5: no timestamp found in: invalid line without timestamp
100%
graph TB
    now[time.Now] --> format[Format]
    now --> sub[Sub/Duration]
    now --> add[Add]
    parse[time.Parse] --> t[time.Time]
    t --> format
    t --> sub
    t --> add
    sub --> duration[time.Duration]
    duration --> hours[Hours/Minutes/Seconds]
    timer[time.Timer] --> C[.C channel]
    ticker[time.Ticker] --> C
    style now fill:#e1f5fe
    style t fill:#e1f5fe
    style duration fill:#f3e5f5
🔥 易错: Go 的时间格式是基于参考时间 Mon Jan 2 15:04:05 MST 2006 的数值模式。2006=年、01=月、02=日、15=时(24h)、03=时(12h)、04=分、05=秒。这个设计很独特,但熟悉后就觉得自然。


❓ 常见问题

Q strings 包有哪些常用函数?
A 裁切(Trim/TrimSpace)、分割(Split/Fields)、merge(Join)、查找(Contains/Index/Count)、替换(Replace/ReplaceAll)、大小写(ToUpper/ToLower)、构造(Builder/Repeat)——这 15 个覆盖 90% 需求。
Q strconv 和类型转换有什么区别?
A 类型转换用于同类转换(int→float64),strconv 用于字符串与数字之间的转换。string 和 int/float 之间不能用类型转换——必须用 strconv。
Q time.Parse 格式字符串怎么写?
A 记住参考时间 2006-01-02 15:04:05(月/日/时/分/秒/年按此顺序)。2006=年、01=月、02=日、15=24小时制、03=12小时制、04=分、05=秒。
Q Duration 怎么用?
A time.Duration 是 int64 纳秒数。创建:5 * time.Second;运算:t.Add(d)t.Sub(t2);获取:d.Hours()d.Minutes()d.Seconds()
Q Timer 和 Ticker 有什么区别?
A Timer 执行一次(延迟执行/超时控制),Ticker 重复执行(定时任务/心跳)。两者都通过 .C channel 接收信号,都支持 .Stop() 停止。
Q Go 有类似 Python 的 datetime.timedelta 吗?
A 有,time.Duration 就是。创建:d := 2*time.Hour + 30*time.Minute;加减:t.Add(d)t.Add(-d);两个时间差:t2.Sub(t1)
Q 如何处理跨时区的时间?
Atime.LoadLocation("Asia/Shanghai") 加载时区,然后 t.In(loc) 转换。内部统一用 UTC 存储,输出时转换为目标时区。
Q strings.Builder 比 + 快多少?
A + 每次创建新字符串 => O(n²);Builder 内部缓冲区 => O(n)。拼接 10,000 次时,Builder 快 1000+ 倍。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 strings 包实现一个 wordCount(s string) map[string]int function,统计字符串中每个单词出现的次数。要求用 Fields + loop + map。

  2. 进阶题(难度⭐⭐):实现一个 timeAgo(t time.Time) string function:返回人类可读的描述("3 分钟前" / "2 小时前" / "昨天" / "3 天前")。要求用 time.Since() + Duration conditional。

  3. 挑战题(难度⭐⭐⭐):实现一个日志聚合工具:输入字符串(每行一个时间戳+级别+消息),用 strings.Split / time.Parse / strings.Builder / sort.Slice 完成:(1) 解析并过滤出 ERROR 级别日志; (2) 按时间排序; (3) 用 Builder 生成一份包含聚合统计的报告。要求支持至少 3 种时间格式。

Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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