在多包中使用记录器/配置 Golang 生产的最佳实践

Posted

技术标签:

【中文标题】在多包中使用记录器/配置 Golang 生产的最佳实践【英文标题】:Using logger/configs in multi packages best practice for Golang productive 【发布时间】:2019-03-24 18:27:16 【问题描述】:

我的项目结构如下:

myGithubProject/
    |---- cmd
      |----- command
        |----- hello.go
    |---- internal
        |----- template
           |----- template.go
        |----- log
          |----- logger.go
    main.go

logtemplate 在同一级别(在内部包下) 在logger.go 中,我使用logrus 作为带有一些配置沙子的记录器,我想在template 包中使用logger.go 对象。 我应该如何以干净的方式做到这一点?

目前我在我的template.go 文件中将它与import logger 一起使用,

internal 包下我有6 更多packages 需要这个logger。他们每个人都依赖它。 (在log 包上),有什么好办法处理它吗?

更新:

如果我有更多需要传递的东西(比如记录器),这里的方法/模式是什么?也许使用dependency injectioninterface ?其他干净的方法...

我需要一些最佳实践完整示例如何在hello.go 文件和template.go 中使用logger

这是我的项目

cliProject/main.go

package main

import (
    "cliProject/cmd"
    "cliProject/internal/logs"
)

func main() 
    cmd.Execute()
    logs.Logger.Error("starting")



**cliProject/cmd/root.go**

package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command
    Use:   "cliProject",
    Short: "A brief description of your application",


func Execute() 
    if err := rootCmd.Execute(); err != nil 
        fmt.Println(err)
    


**cliProject/cmd/serve.go**

package cmd

import (
    "cliProject/internal/logs"
    "fmt"

    "github.com/spf13/cobra"
)

// serveCmd represents the serve command
var serveCmd = &cobra.Command
    Use:   "serve",
    Short: "A brief description of your command",
    Run: func(cmd *cobra.Command, args []string) 
        fmt.Println("serve called")
        startServe()
        stoppingServe()
    ,


func init() 
    rootCmd.AddCommand(serveCmd)
    serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")


func startServe() 
    logs.Logger.Error("starting from function serve")


func stoppingServe() 
    logs.Logger.Error("stoping from function serve")



**cliProject/cmd/start.go**

package cmd

import (
    "cliProject/internal/logs"
    "github.com/spf13/cobra"
)

// startCmd represents the start command
var startCmd = &cobra.Command
    Use:   "start",
    Short: "Start command",
    Run: func(cmd *cobra.Command, args []string) 
        // Print the logs via logger from internal
        logs.Logger.Println("start called inline")
        // example of many functions which should use the logs...
        start()
        stopping()

    ,


func init() 
    logs.NewLogger()
    rootCmd.AddCommand(startCmd)
    startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")


func start() 
    logs.Logger.Error("starting from function start")


func stopping() 
    logs.Logger.Error("stoping from function start")



**cliProject/internal/logs/logger.go**

package logs

import (
    "github.com/sirupsen/logrus"
    "github.com/x-cray/logrus-prefixed-formatter"
    "os"
)

var Logger *logrus.Logger

func NewLogger() *logrus.Logger 

    var level logrus.Level
    level = LogLevel("info")
    logger := &logrus.Logger
        Out:   os.Stdout,
        Level: level,
        Formatter: &prefixed.TextFormatter
            DisableColors:   true,
            TimestampFormat: "2009-06-03 11:04:075",
        ,
    
    Logger = logger
    return Logger


func LogLevel(lvl string) logrus.Level 
    switch lvl 
    case "info":
        return logrus.InfoLevel
    case "error":
        return logrus.ErrorLevel
    default:
        panic("Not supported")
    

这就是它的样子

【问题讨论】:

【参考方案1】:

我总是将*logrus.Logger 明确地传递给需要它的函数(或偶尔的对象)。这避免了奇怪的依赖循环,明确表明日志记录是该函数所做的事情的一部分,并且更容易在其他地方重用该函数或模块。初始日志对象是在我的主函数中创建和配置的(可能在一些命令行参数处理之后)。

【讨论】:

怎么样,你能提供例子吗?另外,如果我有一件或多件事情要通过,你会如何处理它?我不希望我的函数签名会很大......【参考方案2】:

有几种可能性,每种都有自己的权衡。

    显式传递依赖项 传入包含所有依赖项的上下文 将结构用于方法的上下文 使用全局包并导入

它们在不同的情况下都有自己的位置,并且都有不同的权衡:

    这很清楚,但可能会变得非常混乱,并且由于大量依赖项而使您的函数变得混乱。如果您喜欢,它可以让测试轻松模拟。 这是我最不喜欢的选择,因为它一开始很诱人,但很快就变成了一个混合了许多无关问题的神物。避免。 这在很多情况下都非常有用,例如很多人使用这种方式访问​​数据库。如果需要,也很容易模拟。这允许您设置/共享依赖项,而无需在使用时更改代码 - 基本上以一种比传入显式参数更简洁的方式反转控制。 这具有清晰性和正交性的优点。它将要求您为测试与生产添加单独的设置,在使用之前将包初始化为正确的状态。出于这个原因,有些人不喜欢它。

只要使用非常简单的签名,我更喜欢使用包全局方法进行日志记录。我不倾向于测试日志输出,或者经常更改记录器。考虑一下您真正需要从日志中得到什么,以及是否最好只使用内置的日志包,或许可以尝试其中一种方法,看看哪种方法适合您。

为简洁起见,伪代码示例:

// 1. Pass in dependencies explicitly
func MyFunc(log LoggerInterface, param, param)


// 2. Pass in a context with all dependencies

func MyFunc(c *ContextInterface, param, param)

// 3. Use a struct for context to methods

func (controller *MyController) MyFunc(param, param) 
   controller.Logger.Printf("myfunc: doing foo:%s to bar:%s",foo, bar) 


// 4. Use a package global and import

package log 

var DefaultLogger PrintfLogger

func Printf(format, args) DefaultLogger.Printf(format, args)

// usage: 

import "xxx/log"

log.Printf("myfunc: doing foo:%s to bar:%s",foo, bar) 

我更喜欢使用此选项 4 进行日志记录和配置,甚至是 db 访问,因为它是明确的、灵活的,并且允许轻松切换另一个记录器 - 只需更改导入,无需更改接口。哪种计算最好取决于具体情况 - 如果您正在编写一个广泛使用的库,您可能更愿意允许在结构级别设置记录器。

我通常需要在应用启动时进行显式设置,并且始终避免使用 init 函数,因为它们令人困惑且难以测试。

【讨论】:

嗨,谢谢,两个问题 1. 你能在第三个选项中添加一些例子吗? 2.global 是什么意思,项目结构应该如何使用,我如何共享全局?谢谢! 嗨,我放了一个赏金,如果您可以在答案中添加两个推荐部分的示例,那就太好了 我为每种类型添加了一个示例。 3. Struct 这类似于拥有一个用于操作的控制器,比如保存 logger、db 等,然后只使用该控制器上的方法而不是纯函数。它只是隐藏了您的依赖关系,并且可以说更整洁。 4. 全局打包,在日志包中设置一个默认的logger,然后就可以像go自带的默认日志包一样使用简单的功能进行日志了。 这里的部分问题源于尝试使用第三方日志包,它会对此做出自己的固执己见的决定。例如,这排除了选项 4。我会考虑你是否真的需要复杂的日志记录——尝试了很多不同的库后,我选择了更简单的东西(printf 风格)。 Go 作者在这一点上是对的,我认为日志记录不是指标,日志记录不应该很复杂。我希望这会有所帮助。 好的,所以你建议一个简单的导入,我的问题是我的项目是否会在 Github 上,我想从 cmd 访问到 internal 像 logger 这样的包,如果我有问题?我问是因为它在内部【参考方案3】:

在内部包下,我还有 6 个需要这个记录器的包。他们每个人都依赖它。 (在日志包上),有什么好办法处理吗?

一个好的一般原则是尊重应用程序的选择(是否记录)而不是设置政策。

    让你的internal目录中的Go pkgs成为支持包

    只有在出现问题时才会返回error 不会记录(控制台或其他) 不会panic

    让您的应用程序(cmd 目录中的包)决定发生错误时的适当行为(日志/正常关闭/恢复到 100% 完整性)

这将通过仅在特定层进行日志记录来简化开发。注意:记得给应用程序提供足够的上下文来确定操作

internal/process/process.go

package process

import (
    "errors"
)

var (
    ErrNotFound = errors.New("Not Found")
    ErrConnFail = errors.New("Connection Failed")
)

// function Process is a dummy function that returns error for certain arguments received
func Process(i int) error 
    switch i 
    case 6:
        return ErrNotFound
    case 7:
        return ErrConnFail
    default:
        return nil
    

cmd/servi/main.go

package main

import (
    "log"

    p "../../internal/process"
)

func main() 
    // sample: generic logging on any failure
    err := p.Process(6)
    if err != nil 
        log.Println("FAIL", err)
    

    // sample: this application decides how to handle error based on context
    err = p.Process(7)
    if err != nil 
        switch err 
        case p.ErrNotFound:
            log.Println("DOESN'T EXIST. TRY ANOTHER")
        case p.ErrConnFail:
            log.Println("UNABLE TO CONNECT; WILL RETRY LATER")
        
    

如果我有更多需要传递的东西(比如记录器),这里的方法/模式是什么

依赖注入总是一个不错的首选。仅当最简单的实现不够时才考虑其他方法。

下面的代码使用依赖注入和一流的函数传递将templatelogger 包“连接”在一起。

internal/logs/logger.go

package logger

import (
    "github.com/sirupsen/logrus"
    "github.com/x-cray/logrus-prefixed-formatter"
    "os"
)

var Logger *logrus.Logger

func NewLogger() *logrus.Logger 

    var level logrus.Level
    level = LogLevel("info")
    logger := &logrus.Logger
        Out:   os.Stdout,
        Level: level,
        Formatter: &prefixed.TextFormatter
            DisableColors:   true,
            TimestampFormat: "2009-06-03 11:04:075",
        ,
    
    Logger = logger
    return Logger


func LogLevel(lvl string) logrus.Level 
    switch lvl 
    case "info":
        return logrus.InfoLevel
    case "error":
        return logrus.ErrorLevel
    default:
        panic("Not supported")
    

internal/template/template.go

package template

import (
    "fmt"
    "github.com/sirupsen/logrus"
)

type Template struct 
    Name   string
    logger *logrus.Logger


// factory function New accepts a logging function and some data
func New(logger *logrus.Logger, data string) *Template 
    return &Templatedata, logger


// dummy function DoSomething should do something and log using the given logger
func (t *Template) DoSomething() 
    t.logger.Info(fmt.Sprintf("%s, %s", t.Name, "did something"))

cmd/servi2/main.go

package main

import (
    "../../internal/logs"
    "../../internal/template"
)

func main() 
    // wire our template and logger together
    loggingFunc := logger.NewLogger()

    t := template.New(loggingFunc, "Penguin Template")

    // use the stuff we've set up
    t.DoSomething()

希望这会有所帮助。干杯,

【讨论】:

嗨,谢谢。对于你的第一个例子。如果我想在内部使用 cmd 包中的记录器,你会怎么做? 另外请查看我对记录器文件的更新 一个好的做法是将loggertemplate 分开,并在应用程序级别“将它们连接在一起”。它使loggertemplate 保持独立(无依赖关系)并且可独立测试我已经用更完整的示例更新了第二部分。 只是为了验证 :) 推荐您使用 DI 吗?你的最后一个例子? 我已经检查了您的示例并且它有效 :) 问题是现在我想使用我的代码(请参阅问题中的代码)并且我遇到了问题......你能试试吗在您的示例中将简单记录器更改为我的文件?

以上是关于在多包中使用记录器/配置 Golang 生产的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

Golang 生产 Web 应用程序配置

如何使用一些可选模块构建多包节点模块

生产环境中的 Elasticsearch 配置和最佳实践

BigQuery - 获取 1000000 条记录并使用 goLang 对数据进行一些处理

从 Android 的生产代码中删除日志记录?

在多线程 Rails 环境中使用 Redis 的最佳方式是啥? (彪马/Sidekiq)