
标准库
Go 的标准库不是"把常用函数放在一起"的工具箱,它更像是一套已经磨合好的工程积木。命令行参数用 flag,日志用 log,数字转换用 strconv,文本处理用 strings 和 unicode,排序用 sort,数学计算用 math,时间和超时用 time。
这些包单独看都不难,真正有价值的地方在于把它们组合起来。本文不按照字典顺序罗列 API,而是先理解每个包的边界,再实现一个可以运行的 gostat 日志统计工具:它从标准输入或文件读取日志,解析时间和状态码,按照时间窗口筛选记录,统计接口次数和平均响应时间,最后按次数排序输出。
按照 Go 官方文档的习惯,本文每个部分都分为"官方定义 → 最小示例 → 实际项目"。核心判断是:标准库的优势不是 API 数量多,而是接口之间可以直接组合,足以支撑很多中小型工具和服务。
由于 net/http 和 unsafe 的内容足够展开,本文把它们拆成两个独立篇章:
- GO 标准库 · HTTP 高可用 :插件式 HTTP 服务、中间件、超时、健康检查和优雅关闭;
- GO 标准库 · unsafe :内存布局、指针、零拷贝视图和 GC 生命周期。
本文代码按 Go 1.27 标准库整理。示例中的日志顺序、时区和文件路径可能随运行环境变化。
flag:命令行参数
flag 包用于解析命令行参数。官方文档给出的基本流程是:先定义参数,再调用 flag.Parse(),最后读取参数值和剩余的位置参数。
最小示例
package main
import (
"flag"
"fmt"
)
func main() {
name := flag.String("name", "gopher", "user name")
verbose := flag.Bool("verbose", false, "print verbose output")
flag.Parse()
fmt.Println("name:", *name)
fmt.Println("verbose:", *verbose)
fmt.Println("args:", flag.Args())
}
运行:
go run main.go -name=alice -verbose input.log
输出:
name: alice
verbose: true
args: [input.log]
flag.String 和 flag.Bool 返回指针,因为解析阶段需要把结果写回变量。如果不想在业务代码里解引用指针,可以使用 StringVar、BoolVar 等函数:
var name string
flag.StringVar(&name, "name", "gopher", "user name")
flag.Parse()
fmt.Println(name)
FlagSet
全局 flag 适合单命令程序。需要子命令时,应该创建独立的 FlagSet:
func parseServe(args []string) (string, int, error) {
set := flag.NewFlagSet("serve", flag.ContinueOnError)
addr := set.String("addr", ":8080", "listen address")
workers := set.Int("workers", 4, "worker count")
if err := set.Parse(args); err != nil {
return "", 0, err
}
return *addr, *workers, nil
}
flag.ExitOnError 会在解析失败时退出进程,适合简单命令;flag.ContinueOnError 把错误交给调用者,更适合库代码、测试和多子命令程序。
自定义参数类型
自定义参数只需要实现 flag.Value:
type stringList []string
func (s *stringList) String() string {
return strings.Join(*s, ",")
}
func (s *stringList) Set(value string) error {
*s = append(*s, value)
return nil
}
然后注册:
var tags stringList
set.Var(&tags, "tag", "filter tag, can be repeated")
常见误区
- 忘记调用
flag.Parse(),读取到的永远是默认值; - 在包级变量初始化时读取 flag,此时参数还没有解析;
- 在库包中直接使用全局
flag.CommandLine,导致调用方无法控制解析行为; - 把业务配置全部塞进命令行,配置文件和环境变量仍然需要独立处理。
log:日志输出
log 提供一个简单的 Logger 类型和一个预定义的标准 Logger。默认日志输出到标准错误,并带有日期和时间。
package main
import "log"
func main() {
log.Println("server started")
log.Printf("port=%d", 8080)
}
Fatal 会写日志后调用 os.Exit(1),不会执行 defer;Panic 会写日志后触发 panic。服务请求路径中通常不要使用 Fatal,否则一次请求错误可能直接杀掉整个服务。
自定义 Logger
logger := log.New(os.Stdout, "[worker] ", log.Ldate|log.Ltime|log.Lmicroseconds)
logger.Println("task started")
常用标志包括:
| 标志 | 含义 |
|---|---|
log.Ldate |
日期 |
log.Ltime |
时间 |
log.Lmicroseconds |
微秒 |
log.Lshortfile |
文件名和行号 |
log.LUTC |
使用 UTC |
log.Lmsgprefix |
把前缀放在消息前 |
标准 log 没有内置日志级别和结构化字段。需要 JSON 日志时可以使用 Go 标准库的 log/slog;本文为了讲清 log 的基本机制,后面的项目用多个 Logger 表示级别。
用 Logger 区分级别
var (
infoLogger = log.New(os.Stdout, "INFO ", log.Ldate|log.Ltime)
warnLogger = log.New(os.Stderr, "WARN ", log.Ldate|log.Ltime)
errorLogger = log.New(os.Stderr, "ERROR ", log.Ldate|log.Ltime|log.Lshortfile)
)
这不是完整的日志框架,但对于命令行工具、启动脚本和小型服务已经足够。
math:数学计算和特殊值
math 提供 float64 的常量和函数,包含三角函数、指数、对数、取整、绝对值以及 NaN、Inf 判断。
fmt.Println(math.Pi)
fmt.Println(math.Sqrt(2))
fmt.Println(math.Round(3.6))
fmt.Println(math.Abs(-12.5))
浮点数有两个需要单独处理的值:NaN 和无穷大。
value := math.Sqrt(-1)
fmt.Println(math.IsNaN(value))
fmt.Println(math.IsInf(math.Inf(1), 1))
NaN 不等于它自己,所以不能使用 value == math.NaN() 判断。必须使用 math.IsNaN。
金额不要直接用 float64
math.Round(price*100) / 100 可以处理展示层四舍五入,但金额存储仍然更适合使用整数分:
func cents(yuan float64) int64 {
return int64(math.Round(yuan * 100))
}
如果业务涉及精确会计计算,应该进一步采用整数、定点数或专门的 decimal 实现。float64 的二进制表示不能精确表达所有十进制小数。
strconv:字符串和基础类型转换
strconv 负责基础类型与字符串之间的转换。
count, err := strconv.Atoi("42")
if err != nil {
return err
}
text := strconv.Itoa(count)
fmt.Println(text)
常用 API:
| 函数 | 作用 |
|---|---|
Atoi |
十进制字符串转 int |
Itoa |
int 转十进制字符串 |
ParseInt |
字符串转 int64 |
ParseUint |
字符串转 uint64 |
ParseFloat |
字符串转 float64 |
ParseBool |
字符串转 bool |
FormatInt |
整数格式化为字符串 |
FormatFloat |
浮点数格式化为字符串 |
Quote |
转为 Go 字符串字面量 |
解析时要明确进制和位宽:
n, err := strconv.ParseInt("ff", 16, 32)
if err != nil {
return err
}
fmt.Println(n) // 255
ParseInt 返回 int64,第三个参数限制有效位宽。即使程序运行在 64 位机器上,ParseInt("2147483648", 10, 32) 也会返回范围错误。
使用 strconv.AppendInt 可以直接追加到已有字节切片,减少中间字符串:
buffer := make([]byte, 0, 32)
buffer = strconv.AppendInt(buffer, 42, 10)
buffer = append(buffer, ' ')
buffer = strconv.AppendBool(buffer, true)
strings:UTF-8 文本处理
strings 处理 UTF-8 编码的字符串,但字符串本身仍然是字节序列。len("你好") 返回字节数,不是用户看到的字符数。
s := "Go 语言"
fmt.Println(len(s)) // 9,UTF-8 字节数
fmt.Println(utf8.RuneCountInString(s)) // 5,rune 数
常用操作:
fmt.Println(strings.Contains(s, "语言"))
fmt.Println(strings.HasPrefix(s, "Go"))
fmt.Println(strings.ReplaceAll(s, "语言", "language"))
fmt.Println(strings.Join([]string{"go", "std", "lib"}, "/"))
Builder
大量拼接字符串时使用 strings.Builder:
var builder strings.Builder
builder.Grow(64)
for i := 0; i < 3; i++ {
builder.WriteString("item=")
builder.WriteString(strconv.Itoa(i))
builder.WriteByte('\n')
}
fmt.Print(builder.String())
Builder 的零值可用,但复制一个已经使用过的 Builder 是错误用法。把它作为局部变量使用即可。
Cut 和 Fields
before, after, found := strings.Cut("status=200", "=")
if found {
fmt.Println(before, after)
}
fields := strings.Fields(" go standard library ")
fmt.Println(fields) // [go standard library]
Fields 会按照 Unicode 空白切分,比 strings.Split(s, " ") 更适合处理用户输入。
unicode:按字符分类
unicode 的 API 接收 rune。它可以判断字符是否为字母、数字、空白、标点、符号,也可以做大小写转换。
for _, r := range "Go语言 2026!" {
switch {
case unicode.IsLetter(r):
fmt.Printf("%c is letter\n", r)
case unicode.IsDigit(r):
fmt.Printf("%c is digit\n", r)
case unicode.IsSpace(r):
fmt.Printf("space\n")
case unicode.IsPunct(r):
fmt.Printf("%c is punctuation\n", r)
}
}
unicode.IsLetter 能处理中文、日文和其他 Unicode 字母;不要用 r >= 'a' && r <= 'z' 代替它,除非业务明确只支持 ASCII。
使用 unicode.ToLower 处理单个 rune:
r := unicode.ToUpper('g')
fmt.Printf("%c\n", r)
处理完整字符串时通常使用 strings.ToLower 或 strings.Map:
normalized := strings.Map(unicode.ToLower, "Go LANG")
fmt.Println(normalized)
time:时间、持续时间和超时
time 同时处理墙上时间和单调时钟。time.Now() 返回的 Time 通常包含单调时钟信息,Sub、Since 和 Until 可以用它稳定地测量耗时。
start := time.Now()
time.Sleep(20 * time.Millisecond)
fmt.Println(time.Since(start))
时间格式
Go 不使用 yyyy-MM-dd 这类占位符,而使用固定参考时间:
const layout = "2006-01-02 15:04:05"
text := time.Now().Format(layout)
parsed, err := time.Parse(layout, text)
参考时间是:
Mon Jan 2 15:04:05 MST 2006
时区必须明确:
location, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
return err
}
now := time.Now().In(location)
服务端保存时间通常使用 UTC,展示时再转换到用户时区。
Timer、Ticker 和超时
timer := time.NewTimer(100 * time.Millisecond)
defer timer.Stop()
select {
case <-timer.C:
fmt.Println("timeout")
}
周期任务用 Ticker:
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
fmt.Println("tick")
}
不要忘记 Stop,尤其是动态创建的 timer 和 ticker。
sort:排序、查找和稳定性
sort 提供整数、字符串、浮点数的快捷排序,也支持自定义比较函数。
numbers := []int{4, 1, 9, 2}
sort.Ints(numbers)
fmt.Println(numbers)
结构体排序:
type Record struct {
Path string
Status int
Cost time.Duration
}
sort.Slice(records, func(i, j int) bool {
if records[i].Cost == records[j].Cost {
return records[i].Path < records[j].Path
}
return records[i].Cost > records[j].Cost
})
sort.SliceStable 会保留相等元素的原顺序。如果先按优先级排序,再按输入顺序保留相同优先级,应该使用稳定排序。
二分查找要求数据已经有序:
values := []int{10, 20, 30, 40}
index := sort.SearchInts(values, 30)
if index < len(values) && values[index] == 30 {
fmt.Println("found", index)
}
sort.Search 返回第一个满足条件的位置,不保证这个位置的值等于目标,调用者必须再次判断。
实战:实现 gostat 日志统计器
现在把这些包组合起来。日志格式定义为:
2026-10-15T10:00:01Z GET /api/users 200 12ms
2026-10-15T10:00:02Z GET /api/users 200 8ms
2026-10-15T10:00:03Z POST /api/orders 500 90ms
程序支持:
go run . -file access.log -min-status 200 -max-status 499 -top 10
数据结构
type Record struct {
At time.Time
Method string
Path string
Status int
Cost time.Duration
}
type Summary struct {
Path string
Count int
Errors int
Total time.Duration
}
Record 表示一条日志,Summary 表示按照路径聚合后的结果。这种拆分比直接在字符串上统计更容易测试和扩展。
解析一行日志
func parseLine(line string) (Record, error) {
fields := strings.Fields(line)
if len(fields) != 5 {
return Record{}, fmt.Errorf("expected 5 fields, got %d", len(fields))
}
at, err := time.Parse(time.RFC3339, fields[0])
if err != nil {
return Record{}, fmt.Errorf("parse time: %w", err)
}
status, err := strconv.Atoi(fields[3])
if err != nil {
return Record{}, fmt.Errorf("parse status: %w", err)
}
cost, err := time.ParseDuration(fields[4])
if err != nil {
return Record{}, fmt.Errorf("parse duration: %w", err)
}
return Record{
At: at, Method: fields[1], Path: fields[2], Status: status, Cost: cost,
}, nil
}
聚合和排序
func summarize(records []Record) []Summary {
groups := make(map[string]*Summary)
for _, record := range records {
item := groups[record.Path]
if item == nil {
item = &Summary{Path: record.Path}
groups[record.Path] = item
}
item.Count++
item.Total += record.Cost
if record.Status >= 500 {
item.Errors++
}
}
result := make([]Summary, 0, len(groups))
for _, item := range groups {
result = append(result, *item)
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Path < result[j].Path
}
return result[i].Count > result[j].Count
})
return result
}
完整命令行程序
package main
import (
"bufio"
"flag"
"fmt"
"io"
"log"
"os"
"sort"
"strconv"
"strings"
"time"
"unicode"
)
type Record struct {
At time.Time; Method, Path string; Status int; Cost time.Duration
}
type Summary struct {
Path string; Count, Errors int; Total time.Duration
}
func parseLine(line string) (Record, error) {
fields := strings.Fields(line)
if len(fields) != 5 { return Record{}, fmt.Errorf("invalid field count") }
at, err := time.Parse(time.RFC3339, fields[0]); if err != nil { return Record{}, err }
status, err := strconv.Atoi(fields[3]); if err != nil { return Record{}, err }
cost, err := time.ParseDuration(fields[4]); if err != nil { return Record{}, err }
return Record{At: at, Method: fields[1], Path: fields[2], Status: status, Cost: cost}, nil
}
func readRecords(r io.Reader, from, to time.Time) ([]Record, error) {
scanner := bufio.NewScanner(r)
var records []Record
for lineNo := 1; scanner.Scan(); lineNo++ {
line := strings.TrimSpace(scanner.Text())
if line == "" { continue }
record, err := parseLine(line)
if err != nil { return nil, fmt.Errorf("line %d: %w", lineNo, err) }
if !from.IsZero() && record.At.Before(from) { continue }
if !to.IsZero() && record.At.After(to) { continue }
record.Path = strings.TrimSpace(record.Path)
record.Method = strings.ToUpper(record.Method)
record.Path = strings.Map(func(r rune) rune {
if unicode.IsSpace(r) { return -1 }
return r
}, record.Path)
records = append(records, record)
}
if err := scanner.Err(); err != nil { return nil, err }
return records, nil
}
func summarize(records []Record) []Summary {
groups := make(map[string]*Summary)
for _, record := range records {
item := groups[record.Path]
if item == nil { item = &Summary{Path: record.Path}; groups[record.Path] = item }
item.Count++; item.Total += record.Cost
if record.Status >= 500 { item.Errors++ }
}
result := make([]Summary, 0, len(groups))
for _, item := range groups { result = append(result, *item) }
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count { return result[i].Path < result[j].Path }
return result[i].Count > result[j].Count
})
return result
}
func main() {
fileName := flag.String("file", "", "log file, empty means stdin")
minStatus := flag.Int("min-status", 100, "minimum status")
maxStatus := flag.Int("max-status", 599, "maximum status")
top := flag.Int("top", 10, "maximum rows")
fromText := flag.String("from", "", "start time in RFC3339")
toText := flag.String("to", "", "end time in RFC3339")
flag.Parse()
var from, to time.Time
var err error
if *fromText != "" { from, err = time.Parse(time.RFC3339, *fromText); if err != nil { log.Fatal(err) } }
if *toText != "" { to, err = time.Parse(time.RFC3339, *toText); if err != nil { log.Fatal(err) } }
var input io.Reader = os.Stdin
if *fileName != "" {
file, openErr := os.Open(*fileName)
if openErr != nil { log.Fatal(openErr) }
defer file.Close()
input = file
}
records, err := readRecords(input, from, to)
if err != nil { log.Fatal(err) }
filtered := records[:0]
for _, record := range records {
if record.Status >= *minStatus && record.Status <= *maxStatus { filtered = append(filtered, record) }
}
rows := summarize(filtered)
if *top < len(rows) { rows = rows[:*top] }
fmt.Printf("records=%d\n", len(filtered))
for _, row := range rows {
average := time.Duration(0)
if row.Count > 0 { average = row.Total / time.Duration(row.Count) }
fmt.Printf("%-24s count=%-4d errors=%-3d avg=%s\n", row.Path, row.Count, row.Errors, average)
}
}
这里故意把 unicode 放到真实项目里:路径清洗不应该只考虑 ASCII 空格。生产代码还应该限制单行长度、拒绝异常大的日志文件,并把解析错误记录下来后继续处理,而不是因为一行坏数据丢掉整份统计。
运行结果
输入:
2026-10-15T10:00:01Z GET /api/users 200 12ms
2026-10-15T10:00:02Z GET /api/users 200 8ms
2026-10-15T10:00:03Z POST /api/orders 500 90ms
运行:
go run . -file access.log -top 2
输出:
records=3
/api/users count=2 errors=0 avg=10ms
/api/orders count=1 errors=1 avg=90ms
组合这些包时的工程边界
flag只负责把命令行文本解析成配置,不负责执行业务;log记录程序状态,不要把日志字符串当作机器协议;strconv负责严格转换,错误必须检查;strings处理字节序列和 UTF-8 文本,字符语义交给unicode;time.Duration适合超时和耗时,不要用整数猜单位;sort.Search只适用于已经满足单调条件的数据;math的浮点结果要考虑 NaN、Inf 和精度;- 解析、聚合、输出应该分层,避免一个
main函数承担所有逻辑。