文章

日志与错误

日志与错误

包总览

核心类型/函数用途
fmtPrint, Sprint, Errorf格式化 I/O
logLogger, Print, Fatal基础日志
log/slogLogger, Info, Error, With结构化日志(Go 1.21+)
errorsNew, Is, As, Join, Unwrap错误处理(详见 03-错误处理

fmt 包

格式化动词

动词类型说明示例
%v任意默认格式{小徐 28}
%+vstruct带字段名{Name:小徐 Age:28}
%#v任意Go 语法表示main.User{Name:"小徐", Age:28}
%T任意类型名main.User
%%-字面 %%
%d整数十进制42
%b整数二进制101010
%o整数八进制52
%x整数十六进制小写2a
%X整数十六进制大写2A
%c整数Unicode 字符*
%q字符串带引号"hello"
%s字符串原样输出hello
%f浮点小数3.140000
%e浮点科学计数3.140000e+00
%g浮点紧凑表示3.14
%tbool布尔true
%p指针地址0xc0000140a0

宽度与精度

fmt.Sprintf("%5d", 42)      // "   42"(宽度5,右对齐)
fmt.Sprintf("%-5d|", 42)    // "42   |"(左对齐)
fmt.Sprintf("%05d", 42)     // "00042"(零填充)
fmt.Sprintf("%.2f", 3.14159) // "3.14"(精度2)
fmt.Sprintf("%10.2f", 3.14)  // "      3.14"(宽度10精度2)
fmt.Sprintf("%-10.2f|", 3.14) // "3.14      |"

fmt 函数族

// Print 系列(输出到 stdout)
fmt.Print("hello")          // 无换行
fmt.Println("hello")        // 带换行
fmt.Printf("hello %s\n", name)

// Sprint 系列(返回字符串)
s := fmt.Sprint("hello", " ", "world")
s := fmt.Sprintln("hello", "world")
s := fmt.Sprintf("hello %s", name)

// Fprint 系列(写入 io.Writer)
fmt.Fprint(os.Stderr, "error")
fmt.Fprintln(file, "log line")
fmt.Fprintf(w, "status: %d", code)

// Errorf(创建格式化错误)
err := fmt.Errorf("user %s not found", id)
err := fmt.Errorf("query failed: %w", dbErr)  // 包装错误

fmt.Scanner 接口

// 实现 Stringer 接口自定义 %v 输出
type Color int

func (c Color) String() string {
    names := []string{"Red", "Green", "Blue"}
    if int(c) < len(names) {
        return names[c]
    }
    return "Unknown"
}

c := Color(1)
fmt.Printf("%v\n", c)  // "Green"

log 包

import "log"

// 基本用法
log.Println("server started")           // 2026/08/04 20:34:00 server started
log.Printf("user %s logged in", name)
log.Fatal("cannot start server")        // 打印后 os.Exit(1)
log.Panic("unexpected state")           // 打印后 panic

// 自定义 Logger
logger := log.New(os.Stdout, "APP: ", log.LstdFlags|log.Lshortfile)
logger.Println("custom log")            // APP: 2026/08/04 20:34:00 main.go:10: custom log

// 日志标志
log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
标志说明示例输出前缀
Ldate日期2026/08/04
Ltime时间20:34:00
Lmicroseconds微秒20:34:00.123456
Llongfile完整文件路径/home/user/main.go:10
Lshortfile文件名+行号main.go:10
LUTC使用 UTC 时间-
Lmsgprefix前缀在消息前APP: message

写入文件

f, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
defer f.Close()

log.SetOutput(f)  // 全局日志输出到文件
log.Println("this goes to file")

// 同时输出到文件和控制台
io.MultiWriter(f, os.Stdout)
log.SetOutput(io.MultiWriter(f, os.Stdout))

log/slog 包(Go 1.21+)

slog 是 Go 1.21 引入的结构化日志库,支持 JSON/文本输出,是官方推荐的生产级日志方案。

基本用法

import "log/slog"

// 默认 logger(文本格式)
slog.Info("server started", "port", 8080, "env", "production")
// 2026/08/04 20:34:00 INFO server started port=8080 env=production

// 默认 logger(JSON 格式)
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
slog.Info("server started", "port", 8080)
// {"time":"2026-08-04T20:34:00","level":"INFO","msg":"server started","port":8080}

// 日志级别
slog.Debug("debug message", "key", "value")  // 默认不输出
slog.Info("info message")
slog.Warn("warning message")
slog.Error("error message", "err", err)

配置 Handler

// JSON Handler 配置
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level:     slog.LevelDebug,  // 最低级别
    AddSource: true,              // 添加源文件信息
    ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
        // 自定义属性
        if a.Key == slog.TimeKey {
            a.Key = "@timestamp"
        }
        return a
    },
})
logger := slog.New(handler)
slog.SetDefault(logger)

带上下文的日志

// With:添加固定字段(类似 logger.WithField)
logger := slog.With("service", "care-mate", "version", "1.0.0")
logger.Info("request received", "method", "GET", "path", "/api/users")
// {"time":"...","level":"INFO","msg":"request received","service":"care-mate","version":"1.0.0","method":"GET","path":"/api/users"}

// 从 context 提取 trace ID
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        traceID := r.Header.Get("X-Trace-ID")
        logger := slog.With("trace_id", traceID)
        ctx := context.WithValue(r.Context(), "logger", logger)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// 使用 context 中的 logger
func handler(w http.ResponseWriter, r *http.Request) {
    logger := r.Context().Value("logger").(*slog.Logger)
    logger.Info("processing request", "user_id", 123)
}

slog.Attr 与类型安全

// 使用 slog.Attr 明确指定类型
slog.Info("user created",
    slog.Int("user_id", 123),
    slog.String("name", "小徐"),
    slog.Duration("processing_time", elapsed),
    slog.Any("metadata", user.Metadata),
)

// 分组
slog.Info("request",
    slog.Group("user",
        slog.Int("id", 123),
        slog.String("name", "小徐"),
    ),
    slog.Group("response",
        slog.Int("status", 200),
        slog.Int("size", 1024),
    ),
)
// {"msg":"request","user":{"id":123,"name":"小徐"},"response":{"status":200,"size":1024}}

自定义 LogValuer

type User struct {
    ID   int
    Name string
}

// 实现 LogValuer 接口,自定义日志输出
func (u User) LogValue() slog.Value {
    return slog.GroupValue(
        slog.Int("id", u.ID),
        slog.String("name", u.Name),
    )
}

user := User{ID: 1, Name: "小徐"}
slog.Info("user action", "user", user)
// {"msg":"user action","user":{"id":1,"name":"小徐"}}

日志库对比

性能结构化API 风格推荐度
log/slog✓ JSON键值对★★★★★(Go 1.21+首选)
zap最高✓ JSONzap.String("k", v)★★★★★
zerolog最高✓ JSON链式★★★★☆
logrusWithField★★★☆☆(维护模式)
log字符串拼接★★☆☆☆(仅简单场景)

日志最佳实践

实践说明
✅ 用结构化日志JSON 格式,方便 ELK 采集
✅ 日志包含上下文trace_id、user_id 等
✅ 错误日志记录堆栈debug.Stack()slog.Any("stack", ...)
✅ 不要在热路径打日志高频循环中避免 Debug 日志
✅ 日志级别合理生产环境 Info 级别
✅ 敏感信息不记录密码、token、身份证号
✅ 日志轮转用 lumberjack 或外部工具