Go log/slog 如何把请求字段加入结构化日志
来源:17golang原创
时间:2026-09-12 20:37:37 322浏览 收藏
如果每条 Go 请求日志都要补上 trace_id、request_id 和租户编号,最稳妥的做法不是在每次 Info 调用时手写一遍,而是在请求入口创建一个带公共属性的 slog.Logger,随后沿着 context.Context 传递。这样既能保持 JSON 字段统一,也不会把业务函数改成“先拼日志字符串再输出”。
Logger.With适合固化一条请求链路都会出现的字段。- 有上下文时优先使用
InfoContext、ErrorContext或LogAttrs。 - trace 字段可以检索,但密码、Cookie 和完整令牌不能直接写入日志。
先把日志分成请求字段和事件字段
log/slog 的一条记录由消息、级别和键值属性组成。请求字段描述“这条链路是谁”,例如 trace_id、request_id、HTTP 方法和路径;事件字段描述“此刻发生了什么”,例如订单号、耗时或错误。前者应该在请求入口绑定,后者在具体业务点追加。
下面的初始化只负责输出 JSON。示例中的代码和日志都是文章操作示意,字段值请替换成你的网关或链路追踪系统提供的真实值。
package main
import (
"context"
"log/slog"
"net/http"
"os"
)
type loggerKey struct{}
func main() {
// JSONHandler 让采集系统按字段解析日志,而不是依赖字符串切割。
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
slog.SetDefault(slog.New(handler))
http.HandleFunc("/orders", requestLogger(orderHandler))
_ = http.ListenAndServe(":8080", nil) // 示例服务;生产环境要处理返回错误。
}
func requestLogger(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 实际项目应优先读取可信网关注入的 trace_id,缺失时再生成 request_id。
traceID := r.Header.Get("X-Trace-ID")
if traceID == "" {
traceID = "trace-demo"
}
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = "request-demo"
}
reqLog := slog.Default().With(
"trace_id", traceID,
"request_id", requestID,
"http_method", r.Method,
"http_path", r.URL.Path,
)
ctx := context.WithValue(r.Context(), loggerKey{}, reqLog)
next(w, r.WithContext(ctx))
}
}
func orderHandler(w http.ResponseWriter, r *http.Request) {
logger := r.Context().Value(loggerKey{}).(*slog.Logger)
// 事件字段只在真正处理订单的地方追加,避免污染所有请求日志。
logger.InfoContext(r.Context(), "order accepted", "order_id", "ord-1001")
w.WriteHeader(http.StatusAccepted)
}

为什么要用 With,而不是每行重复传参
With 会返回一个使用同一 Handler、但已经附带公共属性的新 Logger。后续日志调用都会带上这些属性,代码只需要在事件位置添加订单号、结果或耗时。它还比每行重复传参更容易统一字段名,避免一处写 requestId、另一处写 request_id。
如果一个子系统还会使用通用键名,可以再调用 WithGroup,例如把支付模块的 id 放到 payment.id 或 JSON 的 payment 对象下,减少跨模块键名冲突。
有 context 时优先用 Context 方法
InfoContext、ErrorContext 和 LogAttrs 接受调用点的上下文,便于自定义 Handler 或链路系统读取当前 span。若你已经把 Logger 放进 context,取出后仍要把同一个请求上下文传给日志方法;不要为了“带字段”而创建一个永远不会取消的新 context。
func loadOrder(ctx context.Context, orderID string) error {
logger, ok := ctx.Value(loggerKey{}).(*slog.Logger)
if !ok {
logger = slog.Default() // 没有请求包装时仍保持可用的兜底行为。
}
// Attr 适合固定类型,也能避免把参数顺序写错。
logger.LogAttrs(ctx, slog.LevelInfo, "loading order",
slog.String("order_id", orderID),
slog.String("component", "order-service"),
)
return nil
}

字段命名和敏感数据要先定规则
建议在团队内固定一份字段表:trace_id 用于跨服务串联,request_id 用于单次入口定位,http_method 与 http_path 用于筛选,order_id 等业务字段只在相关事件中出现。不要把完整 Authorization、Cookie、密码或身份证号放进公共 Logger;需要记录时先脱敏或只保留不可逆摘要。
| 字段类型 | 绑定位置 | 示例 |
|---|---|---|
| 请求公共字段 | 中间件 | trace_id、request_id |
| 路由字段 | 请求入口 | http_method、http_path |
| 业务事件字段 | 具体函数 | order_id、result、latency_ms |
常见问题
直接调用 slog.Info 会丢掉请求字段吗?
如果没有把公共 Logger 设为默认,直接调用顶层函数不会自动获得请求级字段。业务代码应显式使用请求 Logger,或在请求入口调用 slog.SetDefault 之外采用清晰的依赖传递方案。
为什么日志里有重复的 trace_id?
通常是同一个键既通过 With 绑定,又在某次事件调用中再次传入。统一约定公共字段只在入口绑定,事件字段只在事件点追加。
什么时候用 LogAttrs?
字段类型明确、调用频繁或需要减少交替键值参数时可以用它。普通低频日志用 InfoContext 也足够清楚。
-
154 收藏
-
187 收藏
-
348 收藏
-
260 收藏
-
106 收藏
-
371 收藏
-
387 收藏
-
168 收藏
-
189 收藏
-
231 收藏
-
373 收藏
-
Golang · Go教程 | 2小时前 | Go教程 · html/template · text/template · HTML转义 · 模板安全 · Go html/template xss text/template 模板转义427 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习