Go: Go 字符串与时间处理
最后更新:2026-08-26
字符串和时间是所有程序的两大基础数据类型——Go 标准库在这两个领域提供了完备的工具链,无需第三方依赖。
strings 包有 40+ function,strconv 包处理所有类型转换,time 包覆盖时间的一切操作。这节课你将掌握 Go 字符串和时间处理的全套工具。
1. 你将学到
- strings 包常用函数(裁切/分割/替换/查找)
- strconv 类型转换(Atoi/Itoa/Parse)
- strings.Builder 高效字符串拼接
- time.Now / Parse / Format
- time.Duration 时间计算
- time.Timer / time.Ticker 定时器
- 综合案例:日志时间戳解析器
2. 一个运维工程师的真实故事
(1) 痛点:手动解析时间戳,每行日志都出错
Bob 是运维工程师,他需要从服务器日志中提取时间戳统计 QPS:
"每天 50 万行日志,时间戳格式穿插
2026-07-08T10:00:00Z和07/08/2026 10:00:00 AM两种。我用 Python 的datetime写了 20 行,同事看不懂。换 Go 写,结果时间格式字符串查了 10 次文档。"
他写的第一版:
// 坏代码:时间格式硬编码,不灵活
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 的解法:标准库全覆盖
// 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))
}
}
输出:
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 |
Mon Jan 2 15:04:05 MST 2006(01/02/03/04/05/06 正好对应月/日/时/分/秒/年),记住 2006-01-02 15:04:05 就够了。
3. strings 包核心函数
▶ 示例:strings 常用 12 个函数
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)
}
输出:
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) 字符串 ↔ 数字
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 与类型转换的区别
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?
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 完整用法
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))
}
输出:
Name,Age,City
Alice,28,Shanghai
Bob,32,Beijing
Length: 52
+ 拼接字符串每次创建新字符串,O(n²) 性能。strings.Builder 内部维护可变缓冲区,O(n) 性能。大量拼接时差距可达 1000 倍。
6. time package:时间处理
(1) time.Now 与时间格式
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"))
}
▶ 示例:时间解析
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)
}
输出:
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 时间计算
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:单次定时
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:周期定时
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
}
}
}
输出:
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. 时区处理
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. 完整示例:日志时间戳解析器
// 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)
}
预期输出:
=== 日志分析报告 ===
总行数: 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
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
Mon Jan 2 15:04:05 MST 2006 的数值模式。2006=年、01=月、02=日、15=时(24h)、03=时(12h)、04=分、05=秒。这个设计很独特,但熟悉后就觉得自然。
❓ 常见问题
2006-01-02 15:04:05(月/日/时/分/秒/年按此顺序)。2006=年、01=月、02=日、15=24小时制、03=12小时制、04=分、05=秒。time.Duration 是 int64 纳秒数。创建:5 * time.Second;运算:t.Add(d)、t.Sub(t2);获取:d.Hours()、d.Minutes()、d.Seconds()。.C channel 接收信号,都支持 .Stop() 停止。time.Duration 就是。创建:d := 2*time.Hour + 30*time.Minute;加减:t.Add(d)、t.Add(-d);两个时间差:t2.Sub(t1)。time.LoadLocation("Asia/Shanghai") 加载时区,然后 t.In(loc) 转换。内部统一用 UTC 存储,输出时转换为目标时区。+ 每次创建新字符串 => O(n²);Builder 内部缓冲区 => O(n)。拼接 10,000 次时,Builder 快 1000+ 倍。📖 小节
- strings package 40+ 函数覆盖所有字符串操作
- strconv 处理字符串 ↔ 数字/布尔值的转换(类型转换做不到)
strings.Builder高效拼接,避免+的 O(n²) 问题time.Now获取当前时间,Format/Parse格式化/解析- Go 时间格式基于参考时间
2006-01-02 15:04:05 time.Duration是纳秒数,支持运算和比较Timer单次定时,Ticker周期定时- 时区处理:
LoadLocation+In()method
📝 作业
-
基础题(难度⭐):用 strings 包实现一个
wordCount(s string) map[string]intfunction,统计字符串中每个单词出现的次数。要求用Fields+ loop + map。 -
进阶题(难度⭐⭐):实现一个
timeAgo(t time.Time) stringfunction:返回人类可读的描述("3 分钟前" / "2 小时前" / "昨天" / "3 天前")。要求用time.Since()+ Duration conditional。 -
挑战题(难度⭐⭐⭐):实现一个日志聚合工具:输入字符串(每行一个时间戳+级别+消息),用
strings.Split/time.Parse/strings.Builder/sort.Slice完成:(1) 解析并过滤出 ERROR 级别日志; (2) 按时间排序; (3) 用 Builder 生成一份包含聚合统计的报告。要求支持至少 3 种时间格式。