Golang中Logger实现:配置包与自定义日志包之间的循环依赖问题

Golang中Logger实现:配置包与自定义日志包之间的循环依赖问题 大家好,

这是我第一次使用日志记录器。经过一番搜索,我最终选择了 zerolog。 我创建了一个简单的 Web 服务器,它调用一个虚拟的 API 端点来说明我的问题。 示例的 GitHub 仓库:https://github.com/BigBoulard/logger

var (
	router *gin.Engine
)

func main() {
	// 加载环境变量
	conf.LoadEnv()

	// GinMode 在 /.env 文件中被设置为 "debug"
	gin.SetMode(conf.Env.GinMode)
	router = gin.Default()

	// 创建一个控制器,该控制器使用 httpclient 来调用 https://jsonplaceholder.typicode.com/todos
	controller := controller.NewController(
		httpclient.NewClient(),
	)
	router.GET("/todos", controller.GetTodos)

	// 在 /.env 文件中指定的主机和端口上运行路由器
	err := router.Run(fmt.Sprintf("%s:%s", conf.Env.Host, conf.Env.Port))
	if err != nil {
		log.Fatal("application/main", err)
	}
}

正如你已经注意到的,这个服务器使用了环境变量,这些变量需要根据我是在本地工作还是服务器处于生产环境来以不同的方式加载,对吧?为了管理这个,我创建了一个 conf 包,它看起来像这样。

package conf

import (
	"log"
	"os"

	"github.com/joho/godotenv"
)

var Env = &env{}

type env struct {
	AppMode string
	GinMode string
	Host    string
	Port    string
}

var IsLoaded = false

func LoadEnv() {
	// httpclient 和 Gin 服务器都需要环境变量
	// 但我们不想加载两次
	if IsLoaded {
		return
	}

	// 如果不是 "prod"
	if Env.AppMode != "prod" {
		curDir, err := os.Getwd()
		if err != nil {
			log.Fatal(err, "conf", "LoadEnv", "error loading os.Getwd()")
		}
		// 加载 /.env 文件
		loadErr := godotenv.Load(curDir + "/.env")
		if loadErr != nil {
			log.Fatal(loadErr, "conf", "LoadEnv", "can't load env file from current directory: "+curDir)
		}
		Env.GinMode = "debug"
	} else {
		Env.GinMode = "release"
	}

	// 加载环境变量
	Env.AppMode = os.Getenv("APP_MODE")
	Env.Host = os.Getenv("HOST")
	Env.Port = os.Getenv("PORT")

	IsLoaded = true
}

长话短说,我创建了一个 log 包,我会将其包含在每个微服务中。 这个 log 包在启动时使用一个环境变量来确定日志级别并决定是否激活某些功能。log 包看起来像这样:

package log

var l *logger = NewLogger()
const API = "test api"

type logger struct {
	logger zerolog.Logger // 参见 https://github.com/rs/zerolog#leveled-logging
}

func NewLogger() *logger {
    loadEnvFile()
    var zlog zerolog.Logger
	if os.Getenv("APP_ENV") == "dev" {
           // 开发环境设置 ...
        } else {
          // 生产环境设置 ...
       }
return &logger{
  logger: zlog,
}

func Error(path string, err error) {
	l.logger.
		Error().
		Stack().
		Str("path", path).
		Msg(err.Error())
}

func Fatal(path string, err error) {
	l.logger.
		Fatal().
		Stack().
		Str("path", path).
		Msg(err.Error())
}

// 问题:我需要从 conf.load_env.go 中复制 loadEnvFile()
// 因为 conf 使用了 log ... 但反过来,log 也需要 conf,因为它需要环境变量
func loadEnvFile() {
	curDir, err := os.Getwd()
	if err != nil {
		log.Fatal(err, "test api", "App", "gw - conf - LoadEnv - os.Getwd()")
	}
	loadErr := godotenv.Load(curDir + "/.env")
	if loadErr != nil {
		log.Fatal(err, "test api", "conf - LoadEnv", "godotenv.Load("+curDir+"/.env\")")
	}
}

如你所见,我不能使用 conf 包中的 loadEnvFile(),因为它会导致循环依赖,所以我猜可能有更好的实现方式……

非常感谢。


更多关于Golang中Logger实现:配置包与自定义日志包之间的循环依赖问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中Logger实现:配置包与自定义日志包之间的循环依赖问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go中处理配置包与日志包之间的循环依赖,常见的解决方案是使用依赖注入或初始化函数。以下是几种实现方式:

方案一:使用初始化函数分离依赖

log/logger.go:

package log

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

var (
    logger zerolog.Logger
    initialized = false
)

// Init 初始化日志器,需要在main中尽早调用
func Init(env string) {
    if initialized {
        return
    }
    
    // 根据环境配置zerolog
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    
    if env == "dev" {
        logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}).
            Level(zerolog.DebugLevel).
            With().
            Timestamp().
            Caller().
            Logger()
    } else {
        logger = zerolog.New(os.Stdout).
            Level(zerolog.InfoLevel).
            With().
            Timestamp().
            Logger()
    }
    
    initialized = true
}

func Error(path string, err error) {
    logger.Error().
        Stack().
        Str("path", path).
        Msg(err.Error())
}

func Fatal(path string, err error) {
    logger.Fatal().
        Stack().
        Str("path", path).
        Msg(err.Error())
}

// 其他日志级别方法...

conf/load_env.go:

package conf

import (
    "os"
    "github.com/joho/godotenv"
)

var Env = &env{}
var IsLoaded = false

type env struct {
    AppMode string
    GinMode string
    Host    string
    Port    string
}

func LoadEnv() {
    if IsLoaded {
        return
    }
    
    // 先加载环境变量
    if os.Getenv("APP_MODE") != "prod" {
        curDir, _ := os.Getwd()
        godotenv.Load(curDir + "/.env")
    }
    
    // 填充配置
    Env.AppMode = os.Getenv("APP_MODE")
    Env.Host = os.Getenv("HOST")
    Env.Port = os.Getenv("PORT")
    
    if Env.AppMode != "prod" {
        Env.GinMode = "debug"
    } else {
        Env.GinMode = "release"
    }
    
    IsLoaded = true
}

main.go:

func main() {
    // 1. 先加载环境变量
    conf.LoadEnv()
    
    // 2. 使用配置初始化日志
    log.Init(conf.Env.AppMode)
    
    // 3. 设置Gin模式
    gin.SetMode(conf.Env.GinMode)
    router = gin.Default()
    
    // 后续初始化...
}

方案二:使用接口解耦

log/logger.go:

package log

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

type ConfigProvider interface {
    GetAppMode() string
}

type Logger struct {
    logger zerolog.Logger
}

func NewLogger(config ConfigProvider) *Logger {
    var zlog zerolog.Logger
    
    if config.GetAppMode() == "dev" {
        // 开发配置
        zlog = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}).
            Level(zerolog.DebugLevel).
            With().
            Timestamp().
            Caller().
            Logger()
    } else {
        // 生产配置
        zlog = zerolog.New(os.Stdout).
            Level(zerolog.InfoLevel).
            With().
            Timestamp().
            Logger()
    }
    
    return &Logger{logger: zlog}
}

func (l *Logger) Error(path string, err error) {
    l.logger.Error().
        Stack().
        Str("path", path).
        Msg(err.Error())
}

main.go:

func main() {
    // 加载配置
    conf.LoadEnv()
    
    // 创建日志器,传入配置
    logger := log.NewLogger(conf.Env)
    
    // 使用日志器
    controller := controller.NewController(
        httpclient.NewClient(logger),
    )
}

方案三:使用全局变量和延迟初始化

log/logger.go:

package log

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

var (
    globalLogger zerolog.Logger
    once sync.Once
)

func Initialize(env string) {
    once.Do(func() {
        // 根据env初始化globalLogger
        if env == "dev" {
            globalLogger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}).
                Level(zerolog.DebugLevel).
                With().
                Timestamp().
                Caller().
                Logger()
        } else {
            globalLogger = zerolog.New(os.Stdout).
                Level(zerolog.InfoLevel).
                With().
                Timestamp().
                Logger()
        }
    })
}

func GetLogger() zerolog.Logger {
    return globalLogger
}

func Error(path string, err error) {
    globalLogger.Error().
        Stack().
        Str("path", path).
        Msg(err.Error())
}

conf/load_env.go:

package conf

import (
    "os"
    "log"
    "github.com/joho/godotenv"
)

func LoadEnv() {
    if IsLoaded {
        return
    }
    
    // 使用标准库log,避免循环依赖
    if os.Getenv("APP_MODE") != "prod" {
        curDir, err := os.Getwd()
        if err != nil {
            log.Fatal("conf: error getting working directory:", err)
        }
        
        if err := godotenv.Load(curDir + "/.env"); err != nil {
            log.Fatal("conf: error loading .env file:", err)
        }
    }
    
    // 填充配置...
    IsLoaded = true
}

推荐使用方案一,它清晰地分离了初始化顺序,避免了循环依赖,同时保持了代码的简洁性。

回到顶部