①为什么用 logx,而不是 zap / logrus
go-zero 自带日志库 github.com/zeromicro/go-zero/core/logx。99% 的场景不需要引第三方日志库——它已经把微服务最需要的几件事内置了:
统一 JSON 格式
默认输出 JSON,字段固定(time / level / caller / content…),方便 ELK / Loki / Grafana 直接采集。
自动注入链路
配 WithContext(ctx) 后,日志自动带上 trace/span ID,跨服务串成一条调用链。
多通道分流
Info / Error / Slow / Stat / Severe 走不同逻辑,慢请求、统计指标可单独采集。
零额外依赖
与 goctl 生成的服务天然集成,svc.NewServiceContext 里一行 logx.MustSetup 就接管了。
logx 开打;只有要对接特殊日志后端(发到 Kafka、写 ES)才需要自定义 Writer(见 ⑦)。
②快速上手:打印第一行日志
最朴素的用法,和 fmt.Println 一样简单,但输出是结构化 JSON:
package main
import "github.com/zeromicro/go-zero/core/logx"
func main() {
// 用 goctl 生成的服务会由 svc 自动 SetUp,这里手动初始化一次
logx.MustSetup(logx.LogConf{Mode: "console", Encoding: "plain"})
logx.Info("服务启动完成") // 普通信息
logx.Infof("当前在线用户数: %d", 1024) // 带格式化
logx.Error(err) // 记录错误
logx.Errorf("查询用户 %d 失败: %v", uid, err) // 带格式化
}
默认 Encoding: json 时输出(一行):
{"@timestamp":"2026-08-30T22:00:00.123+08:00","level":"info","caller":"main.go:10","content":"服务启动完成"}
@timestamp(ISO 时间)、level、caller(文件名:行号)、content。开发期把 Encoding 设成 plain 看人话,上线再切回 json。③五个「通道」:不只是 info / error
go-zero 把日志分成几条语义不同的通道。理解它们,日志才有「可观测性」:
Info
正常流程:启动、配置加载、关键节点。量不要太大。
Slow
耗时超过 StatSlowThreshold(默认 500ms)的慢操作,方便定位性能瓶颈。
Stat
统计类数据:请求计数、耗时分布。可直接喂监控系统。
Error
出错了但服务还能跑。ErrorStack(err) 会把堆栈一起打出来。
Severe
要命的错误(DB 连不上等)。logx.Must(err) 会直接 panic/退出。
logx.Info("订单创建成功", orderId)
logx.Slow("数据库查询过慢", cost) // 建议配合 WithDuration
logx.Stat("http.request") // 统计埋点
logx.Error(err) // 普通错误
logx.ErrorStack(err) // 错误 + 完整堆栈
logx.Severe("无法连接数据库,服务不可用")
Error / Slow 只会记录,不会让程序崩溃。要「出错即终止」请用 logx.Must(err)——它会写 Severe 日志后 panic 或 os.Exit(1)(由 ExitOnFatal 开关决定)。④结构化日志(带字段)
比起把变量塞进字符串,用字段能让日志被检索(比如按 user_id 过滤)。go-zero 提供两类写法:
写法 A:Infow / Errorw 的 ...LogField 参数
logx.Infow("用户登录",
logx.Field("user_id", uid),
logx.Field("ip", clientIP),
logx.Field("success", ok),
)
logx.Errorw("下单失败",
logx.Field("order_id", orderId),
logx.Field("reason", err.Error()),
)
写法 B:链式 WithFields(先攒字段,多次复用)
logger := logx.WithContext(ctx).WithFields(
logx.Field("trace_id", traceID),
logx.Field("biz", "payment"),
)
logger.Info("开始扣款")
logger.Error(chargeErr) // 这条也会带上上面两个字段
logx.Field(key, value) 返回一个 LogField,不会马上打印,要交给 Infow/Errorw 的变参,或 WithFields 链式调用才会输出。直接 logx.Field(...) 是无效代码。⑤在服务里「串联上下文」——最关键的一步
在 go-zero 的 handler / logic 里,你拿到的 ctx 里已经带着 trace / span ID(来自 OTEL 或内部 trace 中间件)。用 logx.WithContext(ctx) 打日志,这些链路 ID 会自动出现在每条日志里,跨服务一眼就能追到同一次请求。
在 logic 中这样用(l.ctx 是 go-zero 注入的 context):
func (l *OrderLogic) CreateOrder(req *types.Req) (*types.Resp, error) {
// 关键:把 ctx 传进去,日志自动带 trace / span
logx.WithContext(l.ctx).Infof("创建订单 user=%d", req.UserId)
order, err := l.svc.OrderModel.Insert(l.ctx, req)
if err != nil {
// 错误日志同样带链路 ID,方便全链路定位
logx.WithContext(l.ctx).Errorw("插入订单失败",
logx.Field("user_id", req.UserId),
logx.Field("err", err.Error()),
)
return nil, err
}
return &order, nil
}
logx.Info(...) 是不带 trace 的。务必养成 logx.WithContext(ctx).Info(...) 的习惯,否则日志在 ELK 里就追不到是哪次请求了。⑥配置详解:用 yaml 控制日志行为
go-zero 服务里,config.yaml 的 Log 段会由 svc.NewServiceContext 自动 logx.MustSetup,不用手写。常见配置如下:
Log:
ServiceName: order-api # volume 模式会在文件名前缀加服务名+主机名
Mode: console # console(终端) | file(文件) | volume(k8s)
Encoding: json # json(机器读) | plain(人读, 开发用)
Level: info # debug | info | error | severe
Path: logs # 日志目录,默认 logs
TimeFormat: "2006-01-02T15:04:05.000Z07:00"
Rotation: daily # daily(按天) | size(按大小)
KeepDays: 7 # 保留天数
MaxSize: 512 # size 模式下单文件上限 MB
MaxBackups: 10 # size 模式备份数
Compress: false # 是否压缩旧日志
Stat: true # 是否写统计日志
| 字段 | 取值 | 默认值 | 说明 |
|---|---|---|---|
Mode | console / file / volume | console | 输出到终端 / 文件 / k8s 带主机名 |
Encoding | json / plain | json | JSON 便于采集,plain 便于肉眼 |
Level | debug / info / error / severe | info | 低于该级别不输出 |
Path | 字符串 | logs | 文件模式下的目录 |
ServiceName | 字符串 | 空 | volume 模式必需,否则报错 |
Rotation | daily / size | daily | 按天滚动 / 按大小滚动 |
KeepDays | 整数 | 0(全留) | 保留天数,到期删除 |
MaxSize | 整数 MB | 0(不限) | 仅 size 模式生效 |
Compress | true / false | false | 压缩历史日志 |
Stat | true / false | true | 是否输出 Stat 统计日志 |
MaxContentLength | 整数 | 0(不限) | 单条日志内容截断长度 |
Level: error 时,Info / Slow / Stat 都会被屏蔽,只打 Error 和 Severe。调试时临时调成 debug 能看到更多(前提是代码里用了 logx.Debug)。⑦进阶:耗时、全局字段、自定义输出
带耗时:WithDuration
start := time.Now()
// ... 一段业务 ...
logx.WithContext(ctx).WithDuration(time.Since(start)).Slow("慢查询")
// 输出会多一个 duration 字段,配合 Slow 通道做性能监控
全局字段:AddGlobalFields
给之后所有日志统一加上环境/版本等字段,省得每条都写:
logx.AddGlobalFields(
logx.Field("env", "prod"),
logx.Field("version", "v1.3.0"),
)
自定义输出:SetWriter / AddWriter
要把日志同时发到 Kafka、ES 或远程收集器,实现 logx.Writer 接口(Info/Error/Slow/.../Close 几个方法),再 SetWriter 接管,或 AddWriter 叠加到现有输出之上:
type kafkaWriter struct{ prod *kafka.Producer }
func (w *kafkaWriter) Info(v any, fields ...logx.LogField) { w.send("info", v, fields) }
func (w *kafkaWriter) Error(v any, fields ...logx.LogField) { w.send("error", v, fields) }
// ... 实现 Slow/Stat/Severe/Stack/Close/Alert ...
logx.AddWriter(&kafkaWriter{prod: prod}) // 在原有文件/控制台基础上叠加
file 模式把 JSON 日志落盘,再由 Filebeat / Promtail 采集即可,不必自己写 Writer。⑧完整可运行示例
下面是一段可直接放进 internal/logic/*.go 的综合示例,覆盖:配置注入、链路透传、结构化字段、耗时统计、错误分级。
package logic
import (
"context"
"time"
"github.com/zeromicro/go-zero/core/logx"
)
type GreetLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGreetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GreetLogic {
return &GreetLogic{ctx: ctx, svcCtx: svcCtx}
}
func (l *GreetLogic) Greet(name string) (string, error) {
start := time.Now()
// 1) 常规信息,自动带 trace/span(因为传了 l.ctx)
logx.WithContext(l.ctx).Infow("收到问候请求",
logx.Field("name", name),
)
if name == "" {
// 2) 参数错误:用 Error + 字段,方便检索
logx.WithContext(l.ctx).Errorw("name 为空",
logx.Field("name", name),
)
return "", errors.New("name required")
}
// 3) 模拟一次下游调用
msg, err := l.svcCtx.UserRpc.SayHello(l.ctx, name)
if err != nil {
// 4) 下游失败:带堆栈 + 字段,且带链路 ID
logx.WithContext(l.ctx).Errorw("调用 UserRpc 失败",
logx.Field("name", name),
logx.Field("err", err.Error()),
)
return "", err
}
// 5) 统计耗时,慢了进 Slow 通道,正常进 Stat
cost := time.Since(start)
logger := logx.WithContext(l.ctx).WithDuration(cost)
if cost > 500*time.Millisecond {
logger.Slow("问候处理偏慢")
} else {
logger.Info("问候处理完成")
}
return msg, nil
}
⑨常见坑 & 速查
配置没生效
自己写的 main 里忘了 logx.MustSetup;goctl 生成的服务会在 svc.NewServiceContext 自动调用,前提是 config 里嵌了 logx.LogConf。
日志没 trace
直接 logx.Info 不带链路。永远用 logx.WithContext(ctx).Info(...)。
Level 调高丢日志
Level: error 会把 Info/Slow/Stat 全屏蔽,排查时记得调低。
Field 不打印
logx.Field 只是构造器,必须交给 Infow/Errorw 或 WithFields 才会输出。
Error ≠ panic
logx.Error 不会中断程序;要「出错即停」用 logx.Must(err)。
plain vs json
开发用 plain 看人话,生产用 json 便于采集。
logx.Info / Infof 普通信息 ·
logx.Error / Errorf / ErrorStack 错误 ·
logx.Slow / Slowf 慢操作 ·
logx.Stat / Statf 统计 ·
logx.Severe 致命 ·
logx.WithContext(ctx) 链路透传 ·
logx.Field(k, v) 结构化字段 ·
logx.WithDuration(d) 耗时 ·
logx.MustSetup(c) 初始化。
API 以当前 go-zero 版本(master / latest)为准;logx.WithFields(ctx, ...) 旧写法已被 ContextWithFields 取代,新代码请用 WithContext(ctx).WithFields(...) 链式。