Go slog使用入門示例教程
簡介
slog 是 Go 1.21 引入的官方結(jié)構(gòu)化日志庫(Structured Logging)。它結(jié)束了 Go 標(biāo)準(zhǔn)庫只有簡單 log 包的歷史,讓我們可以直接輸出 JSON 或 Key-Value 格式的日志,非常適合對接 ELK、Grafana Loki 等日志分析系統(tǒng)。
相較于第三方日志庫如 zap、logrus,slog 的優(yōu)勢在于:
- 零依賴:作為標(biāo)準(zhǔn)庫的一部分,無需引入第三方依賴
- 官方維護(hù):長期穩(wěn)定,API 變更有 Go 兼容性承諾保障
- 接口簡潔:API 設(shè)計清晰,學(xué)習(xí)成本低
- 可擴(kuò)展:通過自定義 Handler 可以實現(xiàn)各種定制需求
基本使用
slog 用起來非常簡單。默認(rèn)輸出到標(biāo)準(zhǔn)錯誤流(os.Stderr),格式為普通文本。
package main
import (
"fmt"
"log/slog"
)
func main() {
slog.Debug("Hello world")
slog.Info("Hello world")
slog.Warn("Hello world")
slog.Error("Hello world")
slog.Info("this is a message", "name", "zhangsan")
age := 8
slog.Warn(fmt.Sprintf("這是 %d 歲?", age))
}運(yùn)行輸出:
$ go run main.go
2026/02/15 11:52:24 INFO Hello world
2026/02/15 11:52:24 WARN Hello world
2026/02/15 11:52:24 ERROR Hello world
2026/02/15 11:52:24 INFO this is a message name=zhangsan
2026/02/15 11:52:24 WARN 這是 8 歲?
注意:默認(rèn)的
sloglogger 日志級別為INFO,因此Debug級別的日志不會輸出。
日志級別
slog 定義了四個日志級別,從低到高依次為:
| 級別 | 常量 | 說明 |
|---|---|---|
| DEBUG | slog.LevelDebug | 調(diào)試信息,開發(fā)環(huán)境使用 |
| INFO | slog.LevelInfo | 常規(guī)信息 |
| WARN | slog.LevelWarn | 警告信息 |
| ERROR | slog.LevelError | 錯誤信息 |
輸出 JSON 格式
slog 可以輸出 JSON 格式,便于與 ELK、Grafana Loki 等日志系統(tǒng)集成。
以下示例演示了如何修改默認(rèn)的時間戳格式和調(diào)用源輸出格式,并將其設(shè)置為默認(rèn) logger:
package main
import (
"fmt"
"log/slog"
"os"
"time"
)
func main() {
jsonLogger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true, // 添加調(diào)用源信息
Level: slog.LevelDebug, // 設(shè)置日志級別
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// 自定義時間格式
if a.Key == slog.TimeKey {
if t, ok := a.Value.Any().(time.Time); ok {
a.Value = slog.StringValue(t.Format(time.RFC3339))
}
}
// 簡化調(diào)用源信息,只保留文件名和行號
if a.Key == slog.SourceKey {
source := a.Value.Any().(*slog.Source)
shortFile := source.File
for i := len(source.File) - 1; i > 0; i-- {
if source.File[i] == '/' {
shortFile = source.File[i+1:]
break
}
}
return slog.String("source", fmt.Sprintf("%s:%d", shortFile, source.Line))
}
return a
},
}))
jsonLogger.Debug("Hello world")
jsonLogger.Info("Hello world")
jsonLogger.Warn("Hello world")
jsonLogger.Error("Hello world")
jsonLogger.Info("this is a message", "name", "zhangsan")
age := 8
jsonLogger.Warn(fmt.Sprintf("這是 %d 歲?", age))
// 替換默認(rèn) logger
slog.SetDefault(jsonLogger)
slog.Debug("Hello world")
slog.Info("Hello world")
slog.Warn("Hello world")
slog.Error("Hello world")
slog.Info("this is a message", "name", "zhangsan")
age = 9
slog.Warn(fmt.Sprintf("這是 %d 歲?", age))
}運(yùn)行輸出:
$ go run main.go
{"time":"2026-02-15T12:07:32+08:00","level":"DEBUG","source":"main.go:38","msg":"Hello world"}
{"time":"2026-02-15T12:07:32+08:00","level":"INFO","source":"main.go:39","msg":"Hello world"}
{"time":"2026-02-15T12:07:32+08:00","level":"WARN","source":"main.go:40","msg":"Hello world"}
{"time":"2026-02-15T12:07:32+08:00","level":"ERROR","source":"main.go:41","msg":"Hello world"}
{"time":"2026-02-15T12:07:32+08:00","level":"INFO","source":"main.go:43","msg":"this is a message","name":"zhangsan"}
{"time":"2026-02-15T12:07:32+08:00","level":"WARN","source":"main.go:46","msg":"這是 8 歲?"}
{"time":"2026-02-15T12:07:32+08:00","level":"DEBUG","source":"main.go:50","msg":"Hello world"}
{"time":"2026-02-15T12:07:32+08:00","level":"INFO","source":"main.go:51","msg":"Hello world"}
{"time":"2026-02-15T12:07:32+08:00","level":"WARN","source":"main.go:52","msg":"Hello world"}
{"time":"2026-02-15T12:07:32+08:00","level":"ERROR","source":"main.go:53","msg":"Hello world"}
{"time":"2026-02-15T12:07:32+08:00","level":"INFO","source":"main.go:55","msg":"this is a message","name":"zhangsan"}
{"time":"2026-02-15T12:07:32+08:00","level":"WARN","source":"main.go:58","msg":"這是 9 歲?"}
HandlerOptions 詳解
HandlerOptions 提供了三個配置項:
| 字段 | 類型 | 說明 |
|---|---|---|
AddSource | bool | 是否添加調(diào)用源信息(文件名和行號) |
Level | slog.Leveler | 最低日志級別,低于此級別的日志將被忽略 |
ReplaceAttr | func([]string, slog.Attr) slog.Attr | 用于修改或替換屬性的回調(diào)函數(shù) |
With 注入通用屬性
創(chuàng)建 Logger 時,可以用 With 方法為 logger 添加通用屬性。這些屬性會自動附加到每條日志記錄中,適合注入服務(wù)名、環(huán)境、版本等上下文信息。
package main
import (
"fmt"
"log/slog"
"os"
"time"
)
func main() {
jsonLogger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
if t, ok := a.Value.Any().(time.Time); ok {
a.Value = slog.StringValue(t.Format(time.RFC3339))
}
}
if a.Key == slog.SourceKey {
source := a.Value.Any().(*slog.Source)
shortFile := source.File
for i := len(source.File) - 1; i > 0; i-- {
if source.File[i] == '/' {
shortFile = source.File[i+1:]
break
}
}
return slog.String("source", fmt.Sprintf("%s:%d", shortFile, source.Line))
}
return a
},
})).With("logger", "json", "env", "production")
jsonLogger.Debug("Hello world")
jsonLogger.Info("Hello world")
jsonLogger.Warn("Hello world")
jsonLogger.Error("Hello world")
jsonLogger.Info("this is a message", "name", "zhangsan")
}運(yùn)行輸出:
$ go run main.go
{"time":"2026-02-15T13:24:38+08:00","level":"DEBUG","source":"main.go:42","msg":"Hello world","logger":"json","env":"production"}
{"time":"2026-02-15T13:24:38+08:00","level":"INFO","source":"main.go:43","msg":"Hello world","logger":"json","env":"production"}
{"time":"2026-02-15T13:24:38+08:00","level":"WARN","source":"main.go:44","msg":"Hello world","logger":"json","env":"production"}
{"time":"2026-02-15T13:24:38+08:00","level":"ERROR","source":"main.go:45","msg":"Hello world","logger":"json","env":"production"}
{"time":"2026-02-15T13:24:38+08:00","level":"INFO","source":"main.go:47","msg":"this is a message","logger":"json","env":"production","name":"zhangsan"}
使用 Group 對屬性分組
當(dāng)日志屬性較多時,可以使用 slog.Group 將相關(guān)屬性組織在一起,使輸出結(jié)構(gòu)更清晰:
package main
import (
"fmt"
"log/slog"
"os"
"time"
)
func main() {
jsonLogger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
if t, ok := a.Value.Any().(time.Time); ok {
a.Value = slog.StringValue(t.Format(time.RFC3339))
}
}
if a.Key == slog.SourceKey {
source := a.Value.Any().(*slog.Source)
shortFile := source.File
for i := len(source.File) - 1; i > 0; i-- {
if source.File[i] == '/' {
shortFile = source.File[i+1:]
break
}
}
return slog.String("source", fmt.Sprintf("%s:%d", shortFile, source.Line))
}
return a
},
}))
jsonLogger = jsonLogger.With("logger", "json")
// 使用 Group 組織相關(guān)屬性
jsonLogger.Info("系統(tǒng)狀態(tài)",
slog.Group("metrics",
slog.Int("cpu", 4),
slog.Float64("memPercent", 2.33),
),
slog.Group("request",
slog.String("method", "GET"),
slog.String("path", "/api/users"),
),
)
}運(yùn)行輸出:
$ go run main.go
{"time":"2026-02-15T13:30:08+08:00","level":"INFO","source":"main.go:43","msg":"系統(tǒng)狀態(tài)","logger":"json","metrics":{"cpu":4,"memPercent":2.33},"request":{"method":"GET","path":"/api/users"}}
高性能場景使用 LogAttrs
如果需要在高性能循環(huán)中打印日志,建議使用 LogAttrs 方法。它使用強(qiáng)類型屬性(slog.Attr),避免了反射帶來的性能開銷。
package main
import (
"context"
"log/slog"
"os"
"time"
)
func main() {
jsonLogger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
if t, ok := a.Value.Any().(time.Time); ok {
a.Value = slog.StringValue(t.Format(time.RFC3339))
}
}
if a.Key == slog.SourceKey {
source := a.Value.Any().(*slog.Source)
shortFile := source.File
for i := len(source.File) - 1; i > 0; i-- {
if source.File[i] == '/' {
shortFile = source.File[i+1:]
break
}
}
return slog.String("source", fmt.Sprintf("%s:%d", shortFile, source.Line))
}
return a
},
})).With("logger", "json")
for i := range 5 {
jsonLogger.LogAttrs(
context.Background(),
slog.LevelInfo,
"執(zhí)行遍歷",
slog.Int("round", i),
slog.String("task_name", "cleanup"),
slog.Duration("duration", time.Second*time.Duration(i+1)),
)
}
}運(yùn)行輸出:
$ go run main.go
{"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"執(zhí)行遍歷","logger":"json","round":0,"task_name":"cleanup","duration":1000000000}
{"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"執(zhí)行遍歷","logger":"json","round":1,"task_name":"cleanup","duration":2000000000}
{"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"執(zhí)行遍歷","logger":"json","round":2,"task_name":"cleanup","duration":3000000000}
{"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"執(zhí)行遍歷","logger":"json","round":3,"task_name":"cleanup","duration":4000000000}
{"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"執(zhí)行遍歷","logger":"json","round":4,"task_name":"cleanup","duration":5000000000}
性能對比
根據(jù)官方基準(zhǔn)測試,LogAttrs 相比普通方法調(diào)用有約 30% 的性能提升:
| 方法 | 內(nèi)存分配 | 性能 |
|---|---|---|
slog.Info(msg, "key", value) | 有額外分配 | 基準(zhǔn) |
slog.LogAttrs(ctx, level, msg, attrs...) | 零額外分配 | 快約 30% |
提取 Context 中的鏈路信息
slog 提供了 InfoContext、WarnContext 等方法,可以從 context.Context 中提取數(shù)據(jù)。默認(rèn)情況下,這些方法不會自動提取 context 中的值,需要通過自定義 Handler 來實現(xiàn)。
自定義 ContextHandler
以下示例實現(xiàn)了一個自定義 Handler,用于從 context 中提取 TraceID:
package main
import (
"context"
"log/slog"
"os"
)
type contextKey string
const TraceIDKey contextKey = "trace_id"
// ContextHandler 包裝一個 slog.Handler,在處理日志時自動從 context 中提取 TraceID
type ContextHandler struct {
slog.Handler
}
func (h *ContextHandler) Handle(ctx context.Context, record slog.Record) error {
if ctx != nil {
if traceID, ok := ctx.Value(TraceIDKey).(string); ok && traceID != "" {
record.AddAttrs(slog.String(string(TraceIDKey), traceID))
}
}
return h.Handler.Handle(ctx, record)
}
func main() {
baseHandler := slog.NewJSONHandler(os.Stdout, nil)
handler := &ContextHandler{Handler: baseHandler}
jsonLogger := slog.New(handler)
slog.SetDefault(jsonLogger)
ctx := context.WithValue(context.Background(), TraceIDKey, "abc123-def456")
slog.InfoContext(ctx, "hello world")
slog.WarnContext(ctx, "something happened", "user", "zhangsan")
}運(yùn)行輸出:
$ go run main.go | python3 -m json.tool
{
"time": "2026-02-15T13:56:43.086323769+08:00",
"level": "INFO",
"msg": "hello world",
"trace_id": "abc123-def456"
}
{
"time": "2026-02-15T13:56:43.086323769+08:00",
"level": "WARN",
"msg": "something happened",
"user": "zhangsan",
"trace_id": "abc123-def456"
}
在 Gin 框架中使用 slog
在 Gin 中使用 slog 的 context 能力,通常的做法是編寫一個中間件來注入 TraceID,并配合自定義 slog.Handler 來提取它。
package main
import (
"context"
"log/slog"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type contextKey string
const TraceIDKey contextKey = "trace_id"
// ContextHandler 從 context 中提取 TraceID 并添加到日志中
type ContextHandler struct {
slog.Handler
}
func (h *ContextHandler) Handle(ctx context.Context, record slog.Record) error {
if ctx != nil {
if traceID, ok := ctx.Value(TraceIDKey).(string); ok && traceID != "" {
record.AddAttrs(slog.String(string(TraceIDKey), traceID))
}
}
return h.Handler.Handle(ctx, record)
}
// SlogMiddleware 是一個 Gin 中間件,用于注入 TraceID
func SlogMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// 優(yōu)先從請求頭獲取 TraceID,沒有則生成新的
traceID := c.GetHeader("X-Trace-ID")
if traceID == "" {
traceID = uuid.New().String()
}
// 將 TraceID 注入到標(biāo)準(zhǔn)的 context.Context 中
// 注意:Gin 的 c.Set 只在 Gin 內(nèi)部生效,slog 需要標(biāo)準(zhǔn)庫的 Context
ctx := context.WithValue(c.Request.Context(), TraceIDKey, traceID)
c.Request = c.Request.WithContext(ctx)
// 將 TraceID 寫入響應(yīng)頭,方便客戶端追蹤
c.Header("X-Trace-ID", traceID)
c.Next()
// 請求結(jié)束后的匯總?cè)罩?
slog.InfoContext(c.Request.Context(), "Request completed",
slog.String("method", c.Request.Method),
slog.String("path", c.Request.URL.Path),
slog.Int("status", c.Writer.Status()),
slog.Int("body_size", c.Writer.Size()),
slog.Duration("latency", time.Since(start)),
)
}
}
// SlogRecovery 是一個自定義的恢復(fù)中間件
// 它會捕獲 Panic,記錄堆棧信息,并使用 slog.ErrorContext 輸出
func SlogRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 檢查是否是連接中斷(broken pipe)
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
// 獲取堆棧信息
stack := string(debug.Stack())
// 獲取原始請求內(nèi)容
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
slog.ErrorContext(c.Request.Context(), "網(wǎng)絡(luò)連接中斷",
slog.Any("error", err),
slog.String("request", string(httpRequest)),
)
c.Error(err.(error))
c.Abort()
return
}
// 記錄 Panic 詳情
slog.ErrorContext(c.Request.Context(), "Recovery from panic",
slog.Any("error", err),
slog.String("stack", stack),
slog.String("request", string(httpRequest)),
)
ctx := c.Request.Context()
traceID, _ := ctx.Value(TraceIDKey).(string)
// 返回 500 狀態(tài)碼
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "Internal Server Error",
"data": nil,
"timestamp": time.Now().Format(time.RFC3339),
"trace_id": traceID,
})
}
}()
c.Next()
}
}
func main() {
// 初始化 slog
baseHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
handler := &ContextHandler{Handler: baseHandler}
jsonLogger := slog.New(handler)
slog.SetDefault(jsonLogger)
// 使用 gin.New() 而不是 gin.Default(),避免內(nèi)置日志干擾
r := gin.New()
r.Use(SlogMiddleware())
r.Use(SlogRecovery())
r.GET("/ping", func(c *gin.Context) {
slog.InfoContext(c.Request.Context(), "Processing /ping request",
slog.String("user", "zhangsan"),
)
time.Sleep(time.Second * 2)
c.JSON(200, gin.H{"msg": "pong"})
})
r.GET("/panic", func(c *gin.Context) {
slog.InfoContext(c.Request.Context(), "About to panic")
panic("something went wrong")
})
r.Run(":8080")
}運(yùn)行后測試:
$ curl http://localhost:8080/ping
{"msg":"pong"}
$ curl http://localhost:8080/panic
{"code":500,"msg":"Internal Server Error","data":null,"timestamp":"2026-02-15T14:30:00+08:00","trace_id":"xxx-xxx-xxx"}日志輸出文件
寫日志文件一定要注意控制日志文件大小,建議配合系統(tǒng)的logrotate。如果服務(wù)運(yùn)行在kubernetes,建議只輸出控制臺日志,由專門的日志收集平臺去獲取控制臺日志。
基本實現(xiàn)
寫到app.log中
package main
import (
"log/slog"
"os"
)
func main() {
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
handler := slog.NewJSONHandler(logFile, nil)
logger := slog.New(handler)
slog.SetDefault(logger)
slog.Info("hello world")
}配合logrotate。在 /etc/logrotate.d/myapp 創(chuàng)建配置文件
/path/to/app.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate # 復(fù)制后截斷,不需要重啟 Go 程序
}使用lumberjack輪轉(zhuǎn)日志文件
如果不想用系統(tǒng)的 logrotate ,可以使用 lumberjack 包,它提供了更靈活的日志輪轉(zhuǎn)策略。
import "gopkg.in/natefinch/lumberjack.v2"
func initLumberjack() {
rollingFile := &lumberjack.Logger{
Filename: "./logs/app.log",
MaxSize: 100, // 單位 MB
MaxBackups: 3, // 保留舊文件的最大個數(shù)
MaxAge: 28, // 保留舊文件的最大天數(shù)
Compress: true, // 是否壓縮
}
handler := slog.NewJSONHandler(rollingFile, nil)
slog.SetDefault(slog.New(handler))
}同時輸出控制臺和日志文件
go1.26 版本后實現(xiàn)了slog.NewMultiHandler,1.26 前可使用io.multiwriter。
package main
import (
"log/slog"
"os"
)
func main() {
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
fileHandler := slog.NewJSONHandler(logFile, nil)
consoleHandler := slog.NewTextHandler(os.Stdout, nil)
multiHandler := slog.NewMultiHandler(fileHandler, consoleHandler) // slog.NewMultiHandler 需要go1.26.0+版本
logger := slog.New(multiHandler)
slog.SetDefault(logger)
slog.Info("hello world")
}自定義日志級別
除了四個內(nèi)置級別,slog 還支持自定義日志級別 (一般來說默認(rèn)的日志級別已經(jīng)夠用了):
package main
import (
"log/slog"
"os"
)
func main() {
// 定義自定義日志級別
const (
LevelTrace = slog.Level(-8) // 比 Debug 更低
LevelNotice = slog.Level(2) // 介于 Info 和 Warn 之間
LevelFatal = slog.Level(12) // 比 Error 更高
)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: LevelTrace, // 設(shè)置最低級別
}))
logger.Log(nil, LevelTrace, "trace message")
logger.Log(nil, LevelNotice, "notice message")
logger.Log(nil, LevelFatal, "fatal message")
}總結(jié)
slog 作為 Go 官方的結(jié)構(gòu)化日志庫,用起來還是挺方便的。對于新項目,推薦直接使用 slog;對于已有項目,可以逐步遷移,slog 的 API 設(shè)計使得遷移成本很低。
到此這篇關(guān)于Go slog使用入門的文章就介紹到這了,更多相關(guān)go slog使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang中g(shù)in框架綁定解析json數(shù)據(jù)的兩種方法
本文介紹 Golang 的 gin 框架接收json數(shù)據(jù)并解析的2種方法,文中通過代碼示例介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-12-12
Golang 關(guān)于Gin框架請求參數(shù)的獲取方法
Gin是Go語言的Web框架,提供路由和中間件支持,本文介紹如何使用Gin獲取HTTP請求參數(shù),包括URLPath參數(shù)、URLQuery參數(shù)、HTTPBody參數(shù)和Header參數(shù),詳解直接獲取和綁定到結(jié)構(gòu)體兩種方法,幫助開發(fā)者高效處理Web請求2024-10-10
在golang xorm中使用postgresql的json,array類型的操作
這篇文章主要介紹了在golang xorm中使用postgresql的json,array類型的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04
詳解Go如何基于現(xiàn)有的context創(chuàng)建新的context
在?Golang?中,context?包提供了創(chuàng)建和管理上下文的功能,那么在GO語言中如何基于現(xiàn)有的context創(chuàng)建新的context,下面小編就來和大家詳細(xì)聊聊2024-01-01

