拥有如此多独立的代码检查工具的问题在于你必须自己下载每个单独的代码检查工具并管理它们的版本。
此外,依次运行每一个可能会太慢。因此,golangci-lint,一个Go代码检查工具聚合器,可以并行运行代码检查工具,重用
Go 构建缓存,并缓存分析结果,从而在后续运行中大大提高性能,是在 Go
项目中设置代码检查的首选方式。
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
安装完成后,您应该检查已安装的版本:
1 2
➜ ~ golangci-lint version golangci-lint has version v1.55.2 built with go1.21.6 from (unknown, mod sum: "h1:yllEIsSJ7MtlDBwDJ9IMBkyEUz2fYE0b5B8IUgO1oP8=") on (unknown)
您还可以通过以下命令查看所有可用的代码检查器:
1
golangci-lint help linters
输出:
1 2 3 4 5
Enabled by default linters: errcheck: errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] gosimple (megacheck): Linter for Go source code that specializes in simplifying code [fast: false, auto-fix: false] govet (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] ......
// 添加上下文属性到 Record 中,然后调用底层的 handler func(h ContextHandler) Handle(ctx context.Context, r slog.Record) error { if attrs, ok := ctx.Value(slogFields).([]slog.Attr); ok { for _, v := range attrs { r.AddAttrs(v) } }
return h.Handler.Handle(ctx, r) }
// AppendCtx 将 slog 属性添加到提供的上下文中, // 以便在使用此类上下文创建的任何 Record 中都会包含该属性 funcAppendCtx(parent context.Context, attr slog.Attr) context.Context { if parent == nil { parent = context.Background() }
if v, ok := parent.Value(slogFields).([]slog.Attr); ok { v = append(v, attr) return context.WithValue(parent, slogFields, v) }
v := []slog.Attr{} v = append(v, attr) return context.WithValue(parent, slogFields, v) }
该 ContextHandler 结构嵌入了 slog.Handler
接口,并实现了 Handle 方法,以提取存储在提供的上下文中的
Slog 属性。如果找到,它们将被添加到 Record
中,然后调用底层的 Handler 来格式化和输出记录。
for i, v := range frames { f := stackFrame{ Source: filepath.Join( filepath.Base(filepath.Dir(v.File)), filepath.Base(v.File), ), Func: filepath.Base(v.Function), Line: v.Line, }
// User 没有实现 `LogValuer` 接口 type User struct { ID string`json:"id"` FirstName string`json:"first_name"` LastName string`json:"last_name"` Email string`json:"email"` Password string`json:"password"` }
for i := 1; i <= 10; i++ { logger.Info(fmt.Sprintf("a message from the gods: %d", i)) } }
输出:
1 2
{"time":"2023-10-18T19:14:09.820090798+02:00","level":"INFO","msg":"a message from the gods: 4"} {"time":"2023-10-18T19:14:09.820117844+02:00","level":"INFO","msg":"a message from the gods: 5"}
sugar.Info("Hello from Zap logger!") sugar.Infoln( "Hello from Zap logger!", ) sugar.Infof( "Hello from Zap logger! The time is %s", time.Now().Format("03:04 AM"), )
{"level":"info","ts":1684147807.960761,"caller":"zap/main.go:17","msg":"Hello from Zap logger!"} {"level":"info","ts":1684147807.960845,"caller":"zap/main.go:18","msg":"Hello from Zap logger!"} {"level":"info","ts":1684147807.960909,"caller":"zap/main.go:21","msg":"Hello from Zap logger! The time is 11:50 AM"} {"level":"info","ts":1684148355.2692218,"caller":"zap/main.go:25","msg":"User logged in","username":"johndoe","userid":123456,"provider":"google"}
logger.Warn("User account is nearing the storage limit", zap.String("username", "john.doe"), zap.Float64("storageUsed", 4.5), zap.Float64("storageLimit", 5.0), )
1
{"level":"warn","ts":1684166023.952419,"caller":"zap/main.go:46","msg":"User account is nearing the storage limit","username":"john.doe","storageUsed":4.5,"storageLimit":5}
childLogger.Info("redirecting user to admin dashboard") }
注意两个日志中都存在 service 和
requestID
1 2
{"level":"info","ts":1684164941.7644951,"caller":"zap/main.go:52","msg":"user registration successful","service":"userService","requestID":"abc123","username":"john.doe","email":"john@example.com"} {"level":"info","ts":1684164941.764551,"caller":"zap/main.go:57","msg":"redirecting user to admin dashboard","service":"userService","requestID":"abc123"}
您可以使用相同的方法向所有日志添加全局元数据。例如,您可以像这样做,将程序的进程ID和编译程序所使用的
Go 版本包含在所有记录中:
logger.Error("Failed to perform an operation", zap.String("operation", "someOperation"), zap.Error(errors.New("something happened")), // the key will be `error` here zap.Int("retryAttempts", 3), zap.String("user", "john.doe"), )
输出:
1
{"level":"error","ts":1684164638.0570025,"caller":"zap/main.go:47","msg":"Failed to perform an operation","operation":"someOperation","error":"something happened","retryAttempts":3,"user":"john.doe","stacktrace":"main.main\n\t/home/ayo/dev/demo/zap/main.go:47\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:250"}
{"level":"fatal","ts":1684170760.2103574,"caller":"zap/main.go:47","msg":"Something went terribly wrong","context":"main","code":500,"error":"An error occurred","stacktrace":"main.main\n\t/home/ayo/dev/demo/zap/main.go:47\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:250"} exit status 1
for i := 1; i <= 10; i++ { logger.Info("an info message") logger.Warn("a warning") } }
因此,您应该只看到六个日志条目,而不是观察 20 个。
1 2 3 4 5 6
{"level":"info","timestamp":"2023-05-17T16:00:17.611+0100","msg":"an info message"} {"level":"warn","timestamp":"2023-05-17T16:00:17.611+0100","msg":"a warning"} {"level":"info","timestamp":"2023-05-17T16:00:17.611+0100","msg":"an info message"} {"level":"warn","timestamp":"2023-05-17T16:00:17.611+0100","msg":"a warning"} {"level":"info","timestamp":"2023-05-17T16:00:17.611+0100","msg":"an info message"} {"level":"warn","timestamp":"2023-05-17T16:00:17.611+0100","msg":"a warning"}
type SensitiveFieldEncoder struct { zapcore.Encoder cfg zapcore.EncoderConfig }
// EncodeEntry is called for every log line to be emitted so it needs to be // as efficient as possible so that you don't negate the speed/memory advantages // of Zap func(e *SensitiveFieldEncoder) EncodeEntry( entry zapcore.Entry, fields []zapcore.Field, ) (*buffer.Buffer, error) { filtered := make([]zapcore.Field, 0, len(fields))
for _, field := range fields { user, ok := field.Interface.(User) if ok { user.Email = "[REDACTED]" field.Interface = user }
{"level":"info","timestamp":"2023-05-20T09:14:46.043+0100","msg":"an info log","name":"main","user":{"id":"USR-12345","name":"John Doe","email":"john.doe@example.com"}}
{"level":"info","timestamp":"2023-05-20T09:28:31.231+0100","msg":"an info log","name":"main","user":{"id":"USR-12345","name":"John Doe","email":"[REDACTED]"}}
然而,请注意,自定义编码器不会影响使用 With()
方法附加的字段,因此如果您这样做:
1 2
child := logger.With(zap.String("name", "main"), zap.Any("user", u)) child.Info("an info log")
{"level":"info","timestamp":"2023-05-20T09:31:11.919+0100","msg":"an info log","name":"main","user":{"id":"USR-12345","name":"John Doe","email":"john.doe@example.com"}}
将 Zap 用作 Slog 的后端
Go 语言引入了新的结构化日志包Slog后,开始着手在 Zap 中实现
slog.Handler 接口,以便利用 Slog API 与
Zap 后端。这种集成确保了在各种依赖项中日志 API
的一致性,并便于无需大幅更改代码即可无缝切换日志包。
目前为止,Slog 尚未包含在官方的 Go
发布版本中。因此,Zap 与 Slog
的官方集成已经提供在一个单独的模块中,可以使用以下命令进行安装:
上述代码片段仅输出比 INFO
更高级别的日志。您还可以通过环境变量设置最小级别(在本教程后面部分演示)。
将上下文数据添加到日志中
当你调用与所选日志级别相对应的方法(Info()、Debug()等)时,将返回一个
zerolog.Event 类型,该类型代表一个日志事件。这个
Event
类型提供了几个方法,允许你向其上下文添加属性(以键值对的形式),以便日志条目包含足够的上下文数据,帮助你理解事件。例如,当记录在服务器上创建资源时,你可以在日志中包含用户
ID 或客户端信息(如 IP 地址),这样以后通过这些属性轻松过滤日志。
for i := 1; i <= 10; i++ { log.Info().Msgf("a message from the gods: %d", i) } }
在此示例中,配置为每条日志在五次中仅记录一次。这在 for
循环中进行了演示,其中 INFO
消息通常被记录十次,但由于采样,它只被记录两次:
1 2
{"level":"info","time":"2023-09-02T08:05:48+01:00","message":"a message from the gods: 1"} {"level":"info","time":"2023-09-02T08:05:48+01:00","message":"a message from the gods: 6"}
warnSampler := &zerolog.BurstSampler{ Burst: 3, Period: 1 * time.Second, // Log every 5th message after exceeding the burst rate of 3 messages per // second NextSampler: &zerolog.BasicSampler{N: 5}, }
{"level":"error","stack":[{"func":"main","line":"19","source":"main.go"},{"func":"main","line":"250","source":"proc.go"},{"func":"goexit","line":"1594","source":"asm_amd64.s"}],"error":"file open failed!","time":"2023-08-24T21:52:24+01:00","message":"something happened!"}
err := errors.New("failed to connect to database") logger.Fatal().Err(err).Msg("something catastrophic happened!")
输出:
1 2
{"level":"fatal","error":"failed to connect to database","time":"2023-09-01T09:34:49+01:00","message":"something catastrophic happened!"} exit status 1