GO [ 标准库 ]

标准库

Go 的标准库不是"把常用函数放在一起"的工具箱,它更像是一套已经磨合好的工程积木。命令行参数用 flag,日志用 log,数字转换用 strconv,文本处理用 strings 和 unicode,排序用 sort,数学计算用 math,时间和超时用 time。

这些包单独看都不难,真正有价值的地方在于把它们组合起来。本文不按照字典顺序罗列 API,而是先理解每个包的边界,再实现一个可以运行的 gostat 日志统计工具:它从标准输入或文件读取日志,解析时间和状态码,按照时间窗口筛选记录,统计接口次数和平均响应时间,最后按次数排序输出。

按照 Go 官方文档的习惯,本文每个部分都分为"官方定义 → 最小示例 → 实际项目"。核心判断是:标准库的优势不是 API 数量多,而是接口之间可以直接组合,足以支撑很多中小型工具和服务。

由于 net/http 和 unsafe 的内容足够展开,本文把它们拆成两个独立篇章:

本文代码按 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 函数承担所有逻辑。

官方资料

相关推荐
用户8356290780511 小时前
使用 Python 在 Excel 中查找、替换数据并高亮显示
后端·python
半杯咖啡半行码1 小时前
C++11核心特性详解:从C++11到C++20 的现代C++指南
开发语言·c++
2601_966949652 小时前
如何判断实时行情数据是否已经更新?从时间戳、交易时段到行情有效性
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
架构师伯子明2 小时前
QT+OpenGL(3):FBO离屏渲染
开发语言·qt
李福春2 小时前
技术思考问题6:技术演讲该晒成功架构,还是交代失败与取舍?
java·开发语言·架构·computer use·深圳同盟·腾讯云架构师技术同盟·反演思维
变与不变8062 小时前
防抖节流详解
开发语言·前端·javascript
Nturmoils2 小时前
本来只想给内网开个口子,Natapp 却多给了一道防护
后端
136096757232 小时前
静态统计 SQL 时必须连 DELETE 一起看
后端