在Wire依赖注入中创建每个提供程序的日志记录器

huangapple go评论68阅读模式
英文:

Creating Per-Provider Loggers in Wire Dependency Injection

问题

我正在使用github.com/google/wire来进行依赖注入,这是一个我正在开发的开源示例项目的一部分。

在名为interfaces的包中,我有以下接口:

type LoginService interface {
    Login(email, password) (*LoginResult, error)
}

type JWTService interface {
    Generate(user *models.User) (*JWTGenerateResult, error)
    Validate(tokenString string) (*JWTValidateResult, error)
}

type UserDao interface {
    ByEmail(email string) (*models.User, error)
}

我有如下的实现:

type LoginServiceImpl struct {
    jwt interfaces.JWTService
    dao interfaces.UserDao
    logger *zap.Logger
}

func NewLoginService(jwt interfaces.JWTService, dao interfaces.UserDao, \
        logger *zap.Logger) *LoginServiceImpl {
    return &LoginServiceImpl{jwt: jwt, dao: dao, logger: logger }
}

type JWTServiceImpl struct {
    key [32]byte
    logger *zap.Logger
}

func NewJWTService(key [32]byte, logger *zap.Logger) (*JWTServiceImpl, error) {
    r := JWTServiceImpl {
        key: key,
        logger: logger,
    }

    if !r.safe() {
        return nil, fmt.Errorf("unable to create JWT service, unsafe key: %s", err)
    }

    return &r, nil
}

type UserDaoImpl struct {
    db *gorm.DB
    logger *zap.Logger
}

func NewUserDao(db *gorm.DB, logger *zap.Logger) *UserDao {
    return &UserDaoImpl{ db: db, logger: logger }
}

这里省略了其他工厂函数和实现,它们都非常相似。它们可能返回错误或者是不会出错的。

我还有一个有趣的工厂函数用于创建数据库连接,这里只展示接口而不是实现:

func Connect(config interfaces.MySQLConfig) (*gorm.DB, error) { /* ... */ }

现在,让我们来解决问题。在我的命令行入口点,我创建了一个日志记录器:

logger, err := zap.NewDevelopment()

对于上面的每个工厂方法,我需要提供一个日志记录器,而不是相同的日志记录器实例,就像这样调用这些方法一样:

logger, err := zap.NewDevelopment()

// 检查错误

db, err := database.Connect(config)

// 检查错误

userDao := dao.NewUserDao(db, logger.Named("dao.user"))
jwtService, err := service.NewJWTService(jwtKey)

// 检查错误

loginService := service.NewLoginService(jwtService, userDao, logger.Named("service.login"))

我的wire.ProviderSet构造如下:

wire.NewSet(
    wire.Bind(new(interfaces.LoginService), new(*service.LoginServiceImpl)),
    wire.Bind(new(interfaces.JWTService), new(*service.JWTServiceImpl)),
    wire.Bind(new(interfaces.UserDao), new(*dao.UserDaoImpl)),
    service.NewLoginService,
    service.NewJWTService,
    dao.NewUserDao,
    database.Connect,
)

我已经阅读了用户指南、教程和最佳实践,但似乎找不到一种方法来将唯一的zap.Logger路由到每个工厂方法,并将随机的[32]byte路由到JWT服务。

由于我的根日志记录器不是在编译时创建的,而且每个工厂方法都需要自己独特的日志记录器,我该如何告诉wire将这些实例绑定到相应的工厂方法?我很难看到如何将相同类型的自定义实例路由到不同的工厂方法。

总结一下:

Wire似乎更喜欢在编译时完成所有操作,将依赖注入配置存储在静态的包级变量中。对于我大部分的用例来说,这是可以的。

对于我剩下的用例,我需要在运行依赖注入之前手动创建一些实例,并且能够将各种*zap.Logger实例路由到需要它的每个服务中。

实质上,我需要让wire执行services.NewUserDao(Connect(mysqlConfig), logger.Named("dao.user"),但我不知道如何在wire中表达这个,并在运行时合并变量与wire的编译时方法。

wire中如何实现这一点?

英文:

I'm using github.com/google/wire for dependency injection in an open source example project that I'm working on.

I have the following interfaces in a package named interfaces:

type LoginService interface {
    Login(email, password) (*LoginResult, error)
}

type JWTService interface {
    Generate(user *models.User) (*JWTGenerateResult, error)
    Validate(tokenString string) (*JWTValidateResult, error)
}

type UserDao interface {
    ByEmail(email string) (*models.User, error)
}

I have implementations that look like this:

type LoginServiceImpl struct {
    jwt interfaces.JWTService
    dao interfaces.UserDao
    logger *zap.Logger
}

func NewLoginService(jwt interfaces.JWTService, dao interfaces.UserDao, \
        logger *zap.Logger) *LoginServiceImpl {
    return &LoginServiceImpl{jwt: jwt, dao: dao, logger: logger }
}

type JWTServiceImpl struct {
    key [32]byte
    logger *zap.Logger
}

func NewJWTService(key [32]byte, logger *zap.Logger) (*JWTServiceImpl, error) {
    r := JWTServiceImpl {
        key: key,
        logger: logger,
    }

    if !r.safe() {
        return nil, fmt.Errorf("unable to create JWT service, unsafe key: %s", err)
    }

    return &r, nil
}

type UserDaoImpl struct {
    db: *gorm.DB
    logger: *zap.Logger
}

func NewUserDao(db *gorm.DB, logger *zap.Logger) *UserDao {
    return &UserDaoImpl{ db: db, logger: logger }
}

I'll exclude other factory functions and implementations here because they all look very similar. They may return an error or be infallible.

I have one other interesting factory for creating the database connection, which I'll just show the interface and not the implementation:

func Connect(config interfaces.MySQLConfig) (*gorm.DB, error) { /* ... */ }

Now, onto the problem. In my command-line entry-point, I'm creating a logger:

logger, err := zap.NewDevelopment()

For each of the factory methods above, I need to provide a logger and not the same logger instance, rather as if these methods were called as follows:

logger, err := zap.NewDevelopment()

// check err

db, err := database.Connect(config)

// check err

userDao := dao.NewUserDao(db, logger.Named("dao.user"))
jwtService, err := service.NewJWTService(jwtKey)

// check err

loginService := service.NewLoginService(jwtService, userDao, logger.Named("service.login"))

My wire.ProviderSet construction looks like this:

wire.NewSet(
    wire.Bind(new(interfaces.LoginService), new(*service.LoginServiceImpl)),
    wire.Bind(new(interfaces.JWTService), new(*service.JWTServiceImpl)),
    wire.Bind(new(interfaces.UserDao), new(*dao.UserDaoImpl)),
    service.NewLoginService,
    service.NewJWTService,
    dao.NewUserDao,
    database.Connect,
)

I've read through the user guide, the tutorial, and best practices, and I can't seem to find a way to route a unique zap.Logger to each of these factory methods, and routing a random [32]byte for the JWT service.

Since my root logger is not created at compile time, and since each of these factory methods needs its own unique logger, how do I tell wire to bind these instances to the corresponding factory methods? I'm having a tough time seeing how to route custom instances of the same type to disparate factory methods.

In summary:

> Wire seems to favor doing everything at compile-time, storing the dependency injection configuration in a static package-level variable. For most of my use-case, this is okay.
>
> For the rest of my use-case, I need to create some instances manually before running the dependency injection and the ability to route various *zap.Logger instances to each service that needs it.
>
> Essentially, I need to have wire do services.NewUserDao(Connect(mysqlConfig), logger.Named("dao.user"), but I don't know how to express this in wire and merge variables at runtime with wire's compile-time approach.

How do I do this in wire?

答案1

得分: 1

我不会翻译代码部分,但我可以为你翻译其他内容。以下是你提供的内容的翻译:

我不得不在我所做的工作中做一些改变,正如文档中推荐的那样:

> 如果你需要注入一个常见的类型,比如 string,创建一个新的字符串类型以避免与其他提供者发生冲突。例如:
> golang > type MySQLConnectionString string >

添加自定义类型

可以承认,文档非常简洁,但我最终创建了一堆类型:

type JWTKey [32]byte
type JWTServiceLogger *zap.Logger
type LoginServiceLogger *zap.Logger
type UserDaoLogger *zap.Logger

更新生产者函数

我更新了我的生产者方法以接受这些类型,但不需要更新我的结构体:

// LoginServiceImpl 实现 interfaces.LoginService
var _ interfaces.LoginService = (*LoginServiceImpl)(nil)

type LoginServiceImpl struct {
    dao interfaces.UserDao
    jwt interfaces.JWTService
    logger *zap.Logger
}

func NewLoginService(dao interfaces.UserDao, jwt interfaces.JWTService, 
        logger LoginServiceLogger) *LoginServiceImpl {
    return &LoginServiceImpl {
        dao: dao,
        jwt: jwt,
        logger: logger,
    }
}

上面的部分很有道理;给定不同的类型意味着 wire 需要更少的工作来确定。

创建注入器

接下来,我需要创建虚拟注入器,然后使用 wire 生成相应的 wire_gen.go。这并不容易,而且非常不直观。按照文档的指示进行操作时,一切都会出错,并给出非常没有帮助的错误消息。

我有一个 cmd/ 包,我的 CLI 入口点位于 cmd/serve/root.go,可以通过命令行运行 ./api serve。我在 cmd/serve/injectors.go 中创建了我的注入器函数,注意到 // +build wireinject 和接下来的换行符是必需的,以通知 Go 这个文件用于代码生成而不是代码本身。

经过多次尝试和错误之后,我最终得到了以下代码:

// +build wireinject

package serve

import /*...*/

func initializeLoginService(
        config interfaces.MySQLConfig,
        jwtKey service.JWTKey,
        loginServiceLogger service.LoginServiceLogger,
        jwtServiceLogger service.JWTServiceLogger,
        userDaoLogger service.UserDaoLogger,
        databaseLogger database.DatabaseLogger,
    ) (interfaces.LoginService, error) {
    
    wire.Build(
        // 绑定接口到实现
        wire.Bind(new(interfaces.LoginService), new(*service.LoginServiceImpl)),
        wire.Bind(new(interfaces.JWTService), new(*service.JWTServiceImpl)),
        wire.Bind(new(interfaces.UserDao), new(*dao.UserDao)),
        // 服务
        service.NewLoginService,
        service.NewJWTService,
        // 数据访问对象
        dao.NewUserDao,
        // 数据库
        database.Connect,
    )

    return nil, nil
}

wire.Bind 调用告诉 wire 在给定接口的情况下使用哪个实现,因此它将知道应该使用返回 *LoginServiceImplservice.NewLoginService 作为 interfaces.LoginService

wire.Build 调用中的其他实体只是工厂函数。

向注入器传递值

我遇到的问题之一是我试图像 wire.Build 传递值就像文档中描述的那样

> 偶尔,将基本值(通常为 nil)绑定到类型是有用的。您可以将值表达式添加到提供程序集中,而不是使注入器依赖于一次性提供程序函数。
>
> golang > type Foo struct { > X int > } > > func injectFoo() Foo { > wire.Build(wire.Value(Foo{X: 42})) > return Foo{} > } >
>
> ...
>
> 需要注意的是,该表达式将被复制到注入器的包中;对变量的引用将在注入器包的初始化期间求值。如果表达式调用任何函数或从任何通道接收,则 Wire 将生成错误。

这让我感到困惑;它听起来好像你只能在运行注入器时使用常量值,但是在“注入器”部分的文档中有两行

> 与提供程序一样,注入器可以在输入上进行参数化(然后将其发送到提供程序),并且可以返回错误。wire.Build 的参数与 wire.NewSet 相同:它们形成一个提供程序集。这是在代码生成期间用于该注入器的提供程序集。

这些行附带了以下代码:

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

这就是我错过的,也是导致我在这方面浪费了很多时间的原因。在这段代码中,context.Context 似乎没有被传递到任何地方,而且它是一个常见的类型,所以我只是耸了耸肩,没有从中学到什么。

我定义了我的注入器函数以接受 JWT 密钥、MySQL 配置和日志记录器类型的参数:

func initializeLoginService(
        config interfaces.MySQLConfig,
        jwtKey service.JWTKey,
        loginServiceLogger service.LoginServiceLogger,
        jwtServiceLogger service.JWTServiceLogger,
        userDaoLogger service.UserDaoLogger,
        databaseLogger database.DatabaseLogger,
    ) (interfaces.LoginService, error) {
    // ...
    return nil, nil
}

然后,我尝试将它们注入到 wire.Build 中:

wire.Build(
    // ...
    wire.Value(config),
    wire.Value(jwtKey),
    wire.Value(loginServiceLogger),
    // ...
)

当我尝试运行 wire 时,它抱怨这些类型被定义了_两次_。这种行为让我非常困惑,但最终我了解到_wire 自动将所有函数参数发送到 wire.Build 中_。

再次强调:wire 自动将所有注入器函数参数发送到 wire.Build

这对我来说并不直观,但我通过吃一堑长一智,了解到这是 wire 的工作方式。

总结

wire 无法在其依赖注入系统中区分相同类型的值。因此,您需要使用类型定义来使这些简单类型具有类型安全性,例如将 [32]byte 包装为 type JWTKey [32]byte

要将实时值注入到 wire.Build 调用中,只需更改注入器函数签名,将这些值包含在函数参数中,wire 将自动将它们注入到 wire.Build 中。

运行 cd pkg/my/package && wire 在该目录中创建 wire_gen.go,用于定义的注入器。完成后,将来对 go generate 的调用将自动更新 wire_gen.go

我将 wire_gen.go 文件提交到我的版本控制系统(VCS)中,我的 VCS 是 Git,这感觉有点奇怪,因为这些是生成的构建产物,但这似乎是通常的做法。排除 wire_gen.go 可能更有优势,但如果这样做,您将需要找到每个包中包含具有 // +build wireinject 标头的文件,运行 wire 在该目录中,然后运行 go generate 以确保一切正常。

希望这样清楚地解释了 wire 如何处理实际值:使用类型包装器使它们具有类型安全性,然后将它们简单地传递给注入器函数,wire 将完成其余工作。

英文:

I had to change what I was doing somewhat, as is recommended in the documentation:

> If you need to inject a common type like string, create a new string type to avoid conflicts with other providers. For example:
>
> type MySQLConnectionString string
>

Adding Custom Types

The documentation is admittedly very terse, but what I ended up doing is creating a bunch of types:

type JWTKey [32]byte
type JWTServiceLogger *zap.Logger
type LoginServiceLogger *zap.Logger
type UserDaoLogger *zap.Logger

Updating Producer Functions

I updated my producer methods to accept these types, but did not have to update my structs:

// LoginServiceImpl implements interfaces.LoginService
var _ interfaces.LoginService = (*LoginServiceImpl)(nil)

type LoginServiceImpl struct {
    dao interfaces.UserDao
    jwt interfaces.JWTService
    logger *zap.Logger
}

func NewLoginService(dao interfaces.UserDao, jwt interfaces.JWTService, 
        logger LoginServiceLogger) *LoginServiceImpl {
    return &LoginServiceImpl {
        dao: dao,
        jwt: jwt,
        logger: logger,
    }
}

This above part made sense; giving distinct types meant that wire had less to figure out.

Creating an Injector

Next, I had to create the dummy injector and then use wire to generate the corresponding wire_gen.go. This was not easy and very unintuitive. When following the documentation, things kept breaking and giving me very unhelpful error messages.

I have a cmd/ package and my CLI entrypoint lives in cmd/serve/root.go, which is run as ./api serve from the command-line. I created my injector function in cmd/serve/injectors.go, note that // +build wireinject and the following newline are required to inform Go that this file is used for code generation and not code itself.

I ultimately arrived at the following code after much trial and error:

// +build wireinject

package serve

import /*...*/

func initializeLoginService(
        config interfaces.MySQLConfig,
        jwtKey service.JWTKey,
        loginServiceLogger service.LoginServiceLogger,
        jwtServiceLogger service.JWTServiceLogger,
        userDaoLogger service.UserDaoLogger,
        databaseLogger database.DatabaseLogger,
    ) (interfaces.LoginService, error) {
    
    wire.Build(
        // bind interfaces to implementations
        wire.Bind(new(interfaces.LoginService), new(*service.LoginServiceImpl)),
        wire.Bind(new(interfaces.JWTService), new(*service.JWTServiceImpl)),
        wire.Bind(new(interfaces.UserDao), new(*dao.UserDao)),
        // services
        service.NewLoginService,
        service.NewJWTService,
        // daos
        dao.NewUserDao,
        // database
        database.Connect,
    )

    return nil, nil
}

The wire.Bind calls inform wire which implementation to use for a given interface so it will know that service.NewLoginService which returns a *LoginServiceImpl should be used as the interfaces.LoginService.

The rest of the entities in the call to wire.Build are just factory functions.

Passing Values to an Injector

One of the the issues I ran into was that I was trying to pass values into wire.Build like the documentation describes:

> Occasionally, it is useful to bind a basic value (usually nil) to a type. Instead of having injectors depend on a throwaway provider function, you can add a value expression to a provider set.
>
>
> type Foo struct {
> X int
> }
>
> func injectFoo() Foo {
> wire.Build(wire.Value(Foo{X: 42}))
> return Foo{}
> }
>

>
> ...
>
> It's important to note that the expression will be copied to the injector's package; references to variables will be evaluated during the injector package's initialization. Wire will emit an error if the expression calls any functions or receives from any channels.

This is what confused me; it sounded like you could only really use constant values when trying to run an injector, but there are two lines in the docs in the "injectors" section:

> Like providers, injectors can be parameterized on inputs (which then get sent to providers) and can return errors. Arguments to wire.Build are the same as wire.NewSet: they form a provider set. This is the provider set that gets used during code generation for that injector.

These lines are accompanied by this code:

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

This is what I missed and what caused me to lose a lot of time on this. context.Context doesn't seem to be passed anywhere in this code, and it's a common type so I just kind of shrugged it off and didn't learn from it.

I defined my injector function to take arguments for the JWT key, the MySQL config, and the logger types:

func initializeLoginService(
        config interfaces.MySQLConfig,
        jwtKey service.JWTKey,
        loginServiceLogger service.LoginServiceLogger,
        jwtServiceLogger service.JWTServiceLogger,
        userDaoLogger service.UserDaoLogger,
        databaseLogger database.DatabaseLogger,
    ) (interfaces.LoginService, error) {
    // ...
    return nil, nil
}

Then, I attempted to inject them into wire.Build:

wire.Build(
    // ...
    wire.Value(config),
    wire.Value(jwtKey),
    wire.Value(loginServiceLogger),
    // ...
)

When I attempted to run wire, it complained that these types were defined twice. I was very confused by this behavior, but ultimately learned that wire automatically sends all function parameters into wire.Build.

Once again: wire automatically sends all injector function parameters into wire.Build.

This was not intuitive to me, but I learned the hard way that it's the way wire works.

Summary

wire does not provide a way for it to distinguish values of the same type within its dependency injection system. Thus, you need to wrap these simple types with type definitions to let wire know how to route them, so instead of [32]byte, type JWTKey [32]byte.

To inject live values into your wire.Build call, simply change your injector function signature to include those values in the function parameters and wire will automatically inject them into wire.Build.

Run cd pkg/my/package && wire to create wire_gen.go in that directory for your defined injectors. Once this is done, future calls to go generate will automatically update wire_gen.go as changes occur.

I have wire_gen.go files checked-in to my version control system (VCS) which is Git, which feels weird due to these being generated build artifacts, but this seems to be the way that this is typically done. It might be more advantageous to exclude wire_gen.go, but if you do this, you'll need to find every package which includes a file with a // +build wireinject header, run wire in that directory, and then go generate just to be sure.

Hopefully this clears up the way that wire works with actual values: make them type safe with type wrappers, and simply pass them to your injector function, and wire does the rest.

huangapple
  • 本文由 发表于 2021年10月1日 05:06:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/69398824.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定