Skip to content

原生日志模块

注意这个是 go 1.2 以后的版本才有的标准库

默认日志实例与方法

go
package main

import "log/slog"

func main() {
  // 可以直接使用 slog 这个默认的实例
	slog.Debug("[debug] log message") // 默认不输出
	slog.Info("[info] log message")   // 默认的日志输出等级是 info
	slog.Warn("[warn] log message")
	slog.Error("[error] log message")
}

修改日志输出等级

如何修改日志输出等级呢, 这就需要自定义实例, 而不是使用默认的 slog 实例

go
package main

import (
	"log/slog"
	"os"
)

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		AddSource: true,            // 日志输出语句的源代码位置
		Level:     slog.LevelDebug, // 设置日志输出等级
	}))

	logger.Debug("[debug] log message") // 此时的 debug 语句也会输出
	logger.Info("[info] log message")
	logger.Warn("[warn] log message")
	logger.Error("[error] log message")
}

修改日志输出格式

go
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		AddSource: true,            // 日志输出语句的源代码位置
		Level:     slog.LevelDebug, // 设置日志输出等级
	}))

	logger.Debug("[debug] log message")
	logger.Info("[info] log message")
	logger.Warn("[warn] log message")
	logger.Error("[error] log message")

	// slog.Debug("[debug] log message")
	// slog.Info("[info] log message")
	// slog.Warn("[warn] log message")
	// slog.Error("[error] log message")
	// 当我们使用自定义的实例后, 日志输出的格式好像和
	// 默认的不一样, 那么应该如何调整日志的输出格式呢?
	// NewJSONHandler: https://pkg.go.dev/log/slog#JSONHandler
	// 让日志以 JSON 格式输出
	logger2 := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		AddSource: false,           // 日志输出语句的源代码位置
		Level:     slog.LevelDebug, // 设置日志输出等级
	}))
	logger2.Debug("[debug] log message")
	logger2.Info("[info] log message")
	logger2.Warn("[warn] log message")
	logger2.Error("[error] log message")

将程序运行的变量内容写入日志

也就是一些资料中说的: 结构化日志

go
package main

import (
	"log/slog"
	"os"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelDebug, // 设置日志输出等级
	}))

	logger.Debug("hard code string message")

	// 之前都是写死的字符串, 如何将一些程序运行中的变量内容写入日志呢?
	logger.Debug("include some variables string message", slog.Int("id", 1001), slog.String("email", "test@example.com"))
}

修改默认实例

go
package main

import (
	"log/slog"
	"os"
)

func main() {
	// 这是我的自定义实例, 我想让所有的 slog.xxx 都使用这个 logger 的属性
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelDebug, // 设置日志输出等级
	}))

	// 修改默认实例后
	slog.SetDefault(logger)

	// 设置日志使用的属性都是 logger 设置的属性
	// 会输出 debug 信息, 并且输出 json 格式日志
	// 也就是说, 在其他文件中直接使用 slog 也会是 logger 设定的属性
	slog.Debug("[debug] log message")
	slog.Info("[info] log message")
	slog.Warn("[warn] log message")
	slog.Error("[error] log message")
}

日志内容替换

go
package main

import (
	"fmt"
	"log/slog"
	"os"
	"time"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level:     slog.LevelDebug, // 设置日志输出等级
		AddSource: false,           // 输出日志的源代码文件位置
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			fmt.Println(a)
			// a 输出的内容是这样的3个字段
			// time=2026-04-20 15:48:43.222565 +0800 CST
			// level=DEBUG
			// msg=some debug log message

			// 将 time 字段格式为: YYYY-MM-DD HH:MM:SS
			if a.Key == slog.TimeKey {
				a.Value = slog.StringValue(a.Value.Time().Format(time.DateTime))
			}
			return a
		},
	}))

	logger.Debug("some debug log message")
}

日志双写

所谓的日志双写, 其实就是同时输出到: 日志文件标准输出 os.Stdout

go
package main

import (
	"fmt"
	"io"
	"log/slog"
	"os"
)

func main() {
	// app.log: 表示打开 app.log 这个文件
	// os.O_RDWR: 表示打开文件的读写权限
	// os.O_CREATE: 表示如果文件不存在则创建文件
	// os.O_APPEND: 表示如果文件存在则追加写入
	// 0o666: 表示创建文件的权限为 666
	logfile, err := os.OpenFile("app.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
	if err != nil {
		fmt.Println("日志文件创建失败:", err)
		return
	}

	defer logfile.Close()

	// MultiWriter: 多路复用写入器(os.Stdout:标准输出 logfile:日志文件)
	mw := io.MultiWriter(os.Stdout, logfile)
	logger := slog.New(slog.NewJSONHandler(mw, &slog.HandlerOptions{
		Level:     slog.LevelDebug, // 设置日志输出等级
		AddSource: false,           // 输出日志的源代码文件位置
	}))


	// 这个日志会同时输出到 app.log 文件和标准输出
	logger.Debug("some debug log message")
}

自定义日志等级

slog 默认的日志等级有:

  • debug: 调试信息
  • info: 普通日志消息
  • warn: 警告信息
  • error: 错误信息(会让程序退出)
go
package main

import (
	"context"
	"log/slog"
	"os"
)

// 1.自定义 log 等级类型
type CustomLogLevel slog.Level

const (
	// 2.定义 log 等级和值
	PanicLevel CustomLogLevel = 16
	FatalLevel CustomLogLevel = 32
)

// 3. 实现 toString 方法
func (l CustomLogLevel) String() string {
	switch l {
	case PanicLevel:
		return "Panic"
	case FatalLevel:
		return "Fatal"
	default:
		return slog.Level(l).String()
	}
}

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelDebug, // 设置日志输出等级
	}))

	// 4. 使用自定义 log 等级
	logger.Log(context.Background(), slog.Level(PanicLevel), "程序恐慌")
	logger.Log(context.Background(), slog.Level(FatalLevel), "致命错误")
	// {"time":"2026-04-21T20:14:21.989557+08:00","level":"ERROR+8","msg":"程序恐慌"}
	// {"time":"2026-04-21T20:14:21.989699+08:00","level":"ERROR+24","msg":"致命错误"}
	// 输出的 level 字段并不直观, 可以用 ReplaceAttr 替换 level 字段的值

	// 5.使用 ReplaceAttr 替换 level 字段的值
	logger2 := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelDebug, // 设置日志输出等级
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.LevelKey {
				logLvStr := CustomLogLevel(a.Value.Any().(slog.Level)).String()
				a.Value = slog.StringValue(logLvStr)
			}

			return a
		},
	}))

	logger2.Log(context.Background(), slog.Level(PanicLevel), "程序恐慌")
	logger2.Log(context.Background(), slog.Level(FatalLevel), "致命错误")
	// {"time":"2026-04-21T20:19:08.252617+08:00","level":"Panic","msg":"程序恐慌"}
	// {"time":"2026-04-21T20:19:08.252632+08:00","level":"Fatal","msg":"致命错误"}
}

添加固定字段输出

go
package main

import (
	"log/slog"
	"os"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelDebug, // 设置日志输出等级
	}))

	// 注: logger.With 会返回一个新的实例
	logger = logger.With(slog.String("AppName", "slog-demo-app"))

	// 请使用这个新的实例
	// {"time":"2026-04-21T20:24:09.922246+08:00","level":"INFO","msg":"日志信息","AppName":"slog-demo-app"}
	logger.Info("日志信息")
}
go
package main

import (
	"log/slog"
	"os"
)

func main() {
	var handler slog.Handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelDebug,
	})

	// 重新赋值 handler
	handler = handler.WithAttrs([]slog.Attr{
		slog.String("request-client-ip", "127.0.0.1"),
		slog.String("request-id", "mock-request-uuid"),
		slog.String("request-path", "/api/v1/xxx"),
		slog.String("request-method", "GET"),
	})

	logger := slog.New(handler)
	logger.Info("日志信息")
	//	{
	//	  "time": "2026-04-21T20:32:02.805126+08:00",
	//	  "level": "INFO",
	//	  "msg": "日志信息",
	//	  "request-client-ip": "127.0.0.1",
	//	  "request-id": "mock-request-uuid",
	//	  "request-path": "/api/v1/xxx",
	//	  "request-method": "GET"
	//	}
}

日志轮转 Log Rotation

为了避免单个日志文件过大, 方便日志归档/清理

slog 本身并不支持日志轮转相关功能, 可以通过第三方开源库来实现 file-rotatelogs

  • 安装
sh
go get github.com/lestrrat-go/file-rotatelogs
  • 使用
go
package main

import (
	"fmt"
	"io"
	"log/slog"
	"os"
	"time"

	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
)

func main() {
	logfile, err := rotatelogs.New(
		"logs/app-%Y-%m-%d.log",                   // 日志文件命名: 如 logs/app-2025-12-11.log
		rotatelogs.WithMaxAge(24*time.Hour*7),     // 日志保存时间: 7days
		rotatelogs.WithRotationTime(24*time.Hour), // 日志文件轮转时间: 24hours
		rotatelogs.WithLocation(time.Local),       // 日志文件时区: 使用本地时间
		// rotatelogs.ForceNewFile(),                 // 强制打开新文件
		rotatelogs.WithClock(rotatelogs.Local),    // 日志文件时间戳生成函数: 使用本地时间
	)
	if err != nil {
		fmt.Println("日志文件创建失败:", err)
		return
	}

	// 日志双写 + 日志轮转
	mw := io.MultiWriter(os.Stdout, logfile)

	logger := slog.New(slog.NewJSONHandler(mw, &slog.HandlerOptions{
		Level:     slog.LevelDebug,
		AddSource: false,
	}))

	// 这个日志会同时输出到 logs/app-%Y-%m-%d.log 文件和标准输出
	logger.Debug("some debug log message")
}

最后

在一些小的项目中, 使用 slog + file-rotatelogs 也是没有问题的, 但是很遗憾 file-rotatelogs 已经不再维护了, 为了长远考虑, 可以使 用 zap 或者 logrus 来实现日志轮转

Released under the MIT License.