在多包中使用记录器/配置 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
log
和template
在同一级别(在内部包下)
在logger.go
中,我使用logrus
作为带有一些配置沙子的记录器,我想在template
包中使用logger.go
对象。
我应该如何以干净的方式做到这一点?
目前我在我的template.go
文件中将它与import logger
一起使用,
在internal
包下我有6
更多packages
需要这个logger
。他们每个人都依赖它。
(在log
包上),有什么好办法处理它吗?
更新:
如果我有更多需要传递的东西(比如记录器),这里的方法/模式是什么?也许使用dependency injection
? interface
?其他干净的方法...
我需要一些最佳实践完整示例如何在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")
如果我有更多需要传递的东西(比如记录器),这里的方法/模式是什么
依赖注入总是一个不错的首选。仅当最简单的实现不够时才考虑其他方法。
下面的代码使用依赖注入和一流的函数传递将template
和logger
包“连接”在一起。
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
包中的记录器,你会怎么做?
另外请查看我对记录器文件的更新
一个好的做法是将logger
和template
分开,并在应用程序级别“将它们连接在一起”。它使logger
和template
保持独立(无依赖关系)并且可独立测试我已经用更完整的示例更新了第二部分。
只是为了验证 :) 推荐您使用 DI 吗?你的最后一个例子?
我已经检查了您的示例并且它有效 :) 问题是现在我想使用我的代码(请参阅问题中的代码)并且我遇到了问题......你能试试吗在您的示例中将简单记录器更改为我的文件?以上是关于在多包中使用记录器/配置 Golang 生产的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章