Skip to content

15|标准库常用包

Go 的标准库覆盖了文件操作、JSON 处理、时间计算、命令行参数解析和日志输出等场景。很多运维和后端工具不需要引入第三方包,单靠标准库就能完成核心功能。本章介绍最常用的五个包:encoding/jsonos/iotimeflaglog/slognet/http 会在后端开发篇详细展开,这里只做简单提及。

一、JSON 解析与生成

解析 JSON

把 JSON 字节流解析到结构体:

go
package main

import (
	"encoding/json"
	"fmt"
)

type Host struct {
	Name string `json:"name"`
	IP   string `json:"ip"`
	Role string `json:"role"`
}

func main() {
	raw := []byte(`{"name":"web01","ip":"192.168.10.11","role":"web"}`)

	var host Host
	if err := json.Unmarshal(raw, &host); err != nil {
		fmt.Println("parse failed:", err)
		return
	}
	fmt.Println(host.Name, host.IP)
}

json.Unmarshal 的第二个参数必须是指针,因为它需要把解析结果写进变量。

生成 JSON

把结构体转成 JSON 字节流:

go host
	data, err := json.Marshal(host)
	if err != nil {
		fmt.Println("marshal failed:", err)
		return
	}
	fmt.Println(string(data))

json.MarshalIndent 生成带缩进的格式,适合打印或写入文件:

go data,

忽略字段和自定义格式

json:"-" 表示跳过该字段;omitempty 表示零值时跳过:

go
type Result struct {
	Name      string        `json:"name"`
	Cost      time.Duration `json:"-"`              // 不输出
	CostMilli int64         `json:"cost_ms,omitempty"` // 0 时跳过
}

time.Duration 直接序列化会变成纳秒数字,不可读。通常把它转成毫秒或秒再输出,内部字段用 json:"-" 隐藏。

二、文件和系统操作

os 包提供文件读写、目录操作、环境变量和进程信息:

go
package main

import (
	"fmt"
	"os"
)

func main() {
	// 读取整个文件
	data, err := os.ReadFile("/etc/hosts")
	if err != nil {
		fmt.Println("read failed:", err)
		return
	}
	fmt.Println(string(data))

	// 写入文件
	err = os.WriteFile("/tmp/output.txt", []byte("hello"), 0644)
	if err != nil {
		fmt.Println("write failed:", err)
	}
}

os.ReadFile 适合小文件,一次性读进内存。大文件用 os.Open 返回的 *os.File 配合缓冲区逐块读取,避免内存耗尽。

检查文件是否存在:

go _,
		if os.IsNotExist(err) {
			fmt.Println("file not found")
		}

os.IsNotExist 只判断"文件不存在"错误。权限不足、路径不是普通文件等情况需要额外处理。

三、io 包

io 包定义了通用的读写接口。最常用的是 io.Copy,把数据从一个 Reader 复制到 Writer:

go
import "io"

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, in)
	return err
}

io.Copy 内部会分配缓冲区,不需要手动管理。复制大文件时,它的内存占用是稳定的。

io.MultiWriter 把数据同时写到多个目标,常用于"写文件同时打印到屏幕":

go file,
		defer file.Close()
		writer := io.MultiWriter(file, os.Stdout)
		fmt.Fprintln(writer, "log message")

四、时间处理

time 包处理时间和定时器:

go
package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Println(now.Format("2006-01-02 15:04:05"))

	// 解析时间字符串
	t, err := time.Parse("2006-01-02", "2024-06-01")
	if err != nil {
		fmt.Println("parse failed:", err)
		return
	}
	fmt.Println(t)
}

Go 的时间格式化用参考时间 "2006-01-02 15:04:05",而不是 "YYYY-MM-DD" 这种占位符。这个设计来自 Go 诞生时间(2006年1月2日15时04分05秒),需要死记硬背。

计算耗时用 time.Since

go start
		// ... 执行操作
		cost := time.Since(start)
		fmt.Println("cost:", cost)

time.Since 等价于 time.Now().Sub(start),但代码更短。

五、命令行参数

标准库 flag 处理简单参数:

go
package main

import (
	"flag"
	"fmt"
)

func main() {
	host := flag.String("host", "127.0.0.1", "target host")
	port := flag.Int("port", 80, "target port")
	verbose := flag.Bool("verbose", false, "verbose output")
	flag.Parse()

	fmt.Printf("host=%s port=%d verbose=%v\n", *host, *port, *verbose)
}

运行:

bash
go run . -host 192.168.10.11 -port 8080 -verbose

flag 返回的是指针,使用时需要解引用(*host)。参数很多、需要子命令时,可以考虑 cobra 这类第三方库。小工具先用标准库 flag 足够。

flag.Parse() 必须在所有参数定义之后调用,通常在 main 开头。如果参数解析失败(比如传了未定义的参数),程序会打印帮助信息并退出。

六、结构化日志

Go 1.21 以后标准库有 log/slog,支持结构化日志:

go
package main

import (
	"log/slog"
	"os"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	logger.Info(
		"check result",
		"host", "web01",
		"service", "nginx",
		"status", "ok",
	)
}

输出:

json
{"time":"2024-06-01T10:00:00Z","level":"INFO","msg":"check result","host":"web01","service":"nginx","status":"ok"}

结构化日志比纯字符串更适合被日志平台检索。字段名稳定后,按 hostservicestatus 筛选和聚合都更方便。日志字段名一旦确定,后续不要随意变更,否则历史日志和新日志的字段对不上。

七、常见错误

JSON tag 字段名未导出

go
type Host struct {
	name string `json:"name"` // 首字母小写,反射不可见
}

序列化后 name 字段为空。这是新手最常见的 JSON 问题之一。

time.Parse 格式写错

go
t, err := time.Parse("2024-01-01", "2024-06-01") // 错误:格式串应该是参考时间

time.Parse 的第一个参数不是示例时间,而是参考时间 "2006-01-02 15:04:05" 的变体。写错后解析结果完全不对,且不会报错(只会返回解析失败)。

flag 在 Parse 之后定义

go
flag.Parse()
host := flag.String("host", "", "target host") // 太晚,不会被解析

flag.Parse() 只解析在它之前定义的标志。后面定义的标志拿不到值,始终用默认值。

忽略 os.ReadFile 的错误

go data,

文件不存在时 datanil,后续操作可能 panic。