Golang中应该使用全局日志器还是传递实例?

Golang中应该使用全局日志器还是传递实例? 你好,

我是Go语言的新手,模块和包系统对我来说也比较陌生。

我想使用Zerolog(GitHub - rs/zerolog: Zero Allocation JSON Logger)进行日志记录。大多数时候,我看到人们直接调用它,甚至文档也使用全局实例。但这难道不被认为是糟糕的做法吗?我是否应该创建一个日志记录器实例并传递它?(尤其是在测试和模拟方面)

对我来说,在代码中的某个地方调用

zerolog.TimeFieldFormat = zerolog.TimeFormatUnix

并让它修改日志记录器,感觉很奇怪。

Go语言在这方面有所不同吗?我应该直接导入日志记录包并使用它吗?还是说这个例子GitHub - rs/zerolog: Zero Allocation JSON Logger是“干净”的做法?

提前感谢


更多关于Golang中应该使用全局日志器还是传递实例?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

啊,对了,忘记提 slog 了,我已经在所有项目中使用了它好几个月了。

更多关于Golang中应该使用全局日志器还是传递实例?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好 @falco467

当你在不同的包中需要不同的配置时,该怎么办呢?

是的,我也这么想 🙂 我觉得这些例子有点误导人(或者只是过于简化了)。

我会创建一个包级别的单一日志记录器 mypackage.log,它派生自全局实例,并在包内的所有地方使用它。

纯属个人观点,我会尽量避免使用全局变量。 通常,我会在 main() 函数中设置所有需要的内容,并将信息传递给其他组件。

这也会让测试变得更容易。

func main() {
    fmt.Println("hello world")
}

我看不出传递一个实例相比使用全局字段有什么真正的好处。如果你只是定义一个全局变量“logger”并在各处使用它,你也可以像传递引用一样轻松地将其替换为另一个记录器。我认为这会让很多事情变得更简单。

并查看新的结构化日志包 slog 包 - golang.org/x/exp/slog - Go 包,该包将在 1.21 版本(下个月)移入标准库。

在你的 main.go 中创建一个单独的日志记录器实例,并将其传递到各处。这样做还有一个额外的好处:如果你想要更改日志记录器或日志记录选项,只需修改一个地方即可。

// 示例代码:创建并传递日志记录器实例
package main

import (
    "log"
    "os"
)

var logger *log.Logger

func init() {
    logger = log.New(os.Stdout, "APP: ", log.Ldate|log.Ltime|log.Lshortfile)
}

func main() {
    logger.Println("应用程序启动")
    // 将 logger 传递给其他函数或模块
    doSomething(logger)
}

func doSomething(l *log.Logger) {
    l.Println("正在执行某些操作")
}

在Go语言中,关于全局日志器与传递实例的选择,主要取决于项目的具体需求。Zerolog同时支持两种方式,各有适用场景。

全局日志器的使用

Zerolog的全局日志器适合简单的应用场景:

package main

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
    "os"
)

func init() {
    // 配置全局日志器
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    zerolog.SetGlobalLevel(zerolog.InfoLevel)
    log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}

func processData() {
    // 直接使用全局日志器
    log.Info().Msg("Processing data started")
    log.Debug().Str("detail", "some debug info").Msg("Debug detail")
    log.Error().Err(someError).Msg("Processing failed")
}

传递日志器实例

对于需要更好控制或测试的场景,传递实例更合适:

package main

import (
    "github.com/rs/zerolog"
    "os"
)

type Service struct {
    logger zerolog.Logger
}

func NewService(logger zerolog.Logger) *Service {
    return &Service{logger: logger}
}

func (s *Service) Process() {
    s.logger.Info().Msg("Service processing started")
    // 业务逻辑
}

// 在生产环境中使用
func main() {
    logger := zerolog.New(os.Stderr).
        With().
        Timestamp().
        Str("service", "myapp").
        Logger()
    
    svc := NewService(logger)
    svc.Process()
}

// 在测试中模拟
func TestService(t *testing.T) {
    // 使用测试输出
    testLogger := zerolog.New(io.Discard).Level(zerolog.Disabled)
    svc := NewService(testLogger)
    // 测试逻辑
}

混合使用策略

实际项目中常采用混合策略:

package main

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
    "os"
)

// 包级别的默认日志器
var defaultLogger = zerolog.New(os.Stderr).With().Timestamp().Logger()

// 可配置的组件
type Component struct {
    logger zerolog.Logger
}

func NewComponent(logger ...zerolog.Logger) *Component {
    if len(logger) > 0 {
        return &Component{logger: logger[0]}
    }
    return &Component{logger: defaultLogger}
}

func (c *Component) DoWork() {
    c.logger.Info().Msg("Component working")
}

// 使用全局日志器作为后备
func helperFunction() {
    log.Info().Msg("Using global logger")
}

配置管理示例

package config

import (
    "github.com/rs/zerolog"
    "io"
    "os"
)

type LoggerConfig struct {
    Level     zerolog.Level
    Output    io.Writer
    AddCaller bool
}

func NewLogger(cfg LoggerConfig) zerolog.Logger {
    logger := zerolog.New(cfg.Output).
        Level(cfg.Level).
        With().
        Timestamp()
    
    if cfg.AddCaller {
        logger = logger.Caller()
    }
    
    return logger.Logger()
}

// 应用配置
func SetupAppLogger() zerolog.Logger {
    cfg := LoggerConfig{
        Level:     zerolog.InfoLevel,
        Output:    os.Stderr,
        AddCaller: true,
    }
    return NewLogger(cfg)
}

测试友好设计

package service

import (
    "github.com/rs/zerolog"
    "testing"
)

type UserService struct {
    logger zerolog.Logger
    repo   UserRepository
}

// 生产环境构造器
func NewUserService(logger zerolog.Logger, repo UserRepository) *UserService {
    return &UserService{
        logger: logger,
        repo:   repo,
    }
}

// 测试辅助函数
func NewTestUserService(t *testing.T, repo UserRepository) *UserService {
    testLogger := zerolog.New(io.Discard).Level(zerolog.Disabled)
    return NewUserService(testLogger, repo)
}

func TestUserService(t *testing.T) {
    mockRepo := &MockUserRepository{}
    svc := NewTestUserService(t, mockRepo)
    // 测试逻辑
}

选择依据:

  • 小型工具或简单应用:全局日志器
  • 大型项目或微服务:传递实例
  • 需要测试和模拟:传递实例
  • 库或框架:提供两种方式

Zerolog文档中的示例展示了两种模式,传递实例的方式在需要控制输出、级别或进行测试时确实是更清晰的做法。

回到顶部