【问题标题】:Different packages with different config props - Functional option具有不同配置道具的不同软件包 - 功能选项
【发布时间】:2018-03-08 00:13:51
【问题描述】:

我有一个需要配置的应用程序,我已经创建了一个配置结构,并且我正在输入配置作为函数的参数。问题是配置结构变得越来越大(如单体),我将配置移动到我的应用程序中的不同功能,并且不需要所有字段,只需要其中的几个。我的问题是是否有更好的方法在 Go 中实现它。

在努力寻找好方法后,我找到了这篇文章(有点旧,但希望仍然有用),我想知道如何以及是否可以使用它来解决我的问题。

功能选项而不是配置结构 https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

我需要在我的应用程序中注入一些配置属性

例如对于函数run(这是入口点)我需要注入log level和其他一些环境变量,如porthost

对于函数build,我需要“注入”build flavorbuild type 等。

我的内容的任何示例都会非常有帮助

  1. 如何在代码中构造它?
  2. 如何实现?

更新

我需要一些 E2E 示例,如何将功能方法用于同一包和其他包中的不同配置

【问题讨论】:

  • 为不同的功能创建一个包含单独结构的文件,并根据您的 main.go 文件中的要求导入它
  • @Himanshu - 当然我可以做到,但我的问题是是否有更好的方法?
  • 发布一些您迄今为止尝试过的代码以获得一个想法。你真正想要什么以及如何
  • 每个函数将使用一个结构体,或者同一个结构体可以用于不同的函数?
  • @Himanshu - 每个结构都可以用于不同的功能,但不是强制性的。有一个选项可以在一个函数中使用一个结构......

标签: go


【解决方案1】:

听起来您正在寻找一种替代方法,以将相同的配置整体结构传递给每个包和每个函数。这个问题有很多解决方案(比我要在这里列出的更多),哪一个适合你,需要比我们更多地了解你的代码和你的目标,所以最好由你决定。听起来您想知道 Dave Cheney 关于功能选项的帖子是否提供了解决方案以及如何应用它。

如果您的应用程序的配置是静态的,因为它不可能通过不同的执行线程更改(变异),并且您不需要在同一个main 中创建具有不同配置的多个实例,那么一个选项是包级别变量和包初始化。如果您反对导出的包变量,您可以使用未导出的包变量并通过导出的函数控制访问。假设runbuild 是两个不同的包:

// package main
import(
    "github.com/profilename/appname/build"
    "github.com/profilename/appname/run"
)
func main() {
    // do something to get configuration values
    build.Initialize(buildFlavor, buildType)
    // any post-build-initialize-pre-run-initialize stuff
    run.Initialize(logLevel, port, host)
    // other processing
    build.PreBuild("title") // other build functions may rely on configuration
    build.Build()
    // other stuff
    run.ReadFiles(f1, f2)
    run.Validate(preferredBackupPort) // port availability, chance to log.Fatal out
    run.Run()
    // cleanup
}

// package run
var Host string
var LogLevel, Port int
init() {
    Host = `localhost`
    Port = 8888
    Loglevel = 1
}
func Initialize(logLevel, port int, host string) {
    // validation, panic on failure
    LogLevel = logLevel
    Host = host
    Port = port
}
func Run() {
    // do something with LogLevel, Host, Port
}

但这并不能解决 Dave Cheney 的帖子中提到的问题。如果用户在没有主机、端口或 buildType(或其他配置变量)的情况下运行它,因为他不需要这些功能怎么办?如果用户想要运行具有不同配置的多个实例怎么办?

Dave 的方法主要用于不使用包级变量进行配置的情况。实际上,它旨在启用事物的多个实例,其中每个实例可以具有不同的配置。您的可选配置参数成为单个可变参数,其中类型是修改指向正在配置的事物的指针的函数。对你来说,可能是

// package run
type Runner struct {
    Port        int
    // rest of runner configuration
}
func NewRunner(options ...func(*Runner)) (runner *Runner, err error) {
    // any setup
    for _, option := range options {
        err = option(runner)
        if err != nil {
            // do something
        }
    }
    return runner, err
}

// package main
func main() {
    // do something to get configuration values
    port := func(runner *Runner) {
        runner.Port = configuredPort
    }
    // other configuration if applicable
    runner := run.NewRunner(port)
    // ...

在某种程度上,Dave 的方法似乎针对将用作非常灵活的库的包,并将提供用户可能希望创建多个实例的应用程序接口。它允许main 定义启动具有不同配置的多个实例。在那篇文章中,他没有详细介绍如何处理main 或配置包中的配置输入。

请注意,上面生成的代码中设置端口的方式与此没有太大区别:

// package run
type Runner struct {
    Port        int
    // rest of runner configuration
}

// package main, func main()
    runner := new(run.Runner)
    runner.Port = configuredPort

这是更传统的方法,可能对大多数开发人员来说更容易阅读和理解,如果它适合您的需求,也是一种非常好的方法。 (如果需要,您可以使 runner.port 未导出并添加 func (r *Runner) SetPort(p int) { r.port = p } 方法。)它也是一种设计,根据实现,有可能处理变异配置、多个执行线程(您需要通道或sync 包来处理那里的突变),以及多个实例。

Dave 提出的功能选项设计比该方法更强大的地方在于,当您有更多与要放置在 main 而不是 run 中的选项设置相关的语句时——那些将组成函数体。


更新 这是一个使用 Dave 的功能选项方法的可运行示例,包含在两个文件中。请务必更新导入路径以匹配您放置 run 包的位置。

run:

package run

import(
    "fmt"
    "log"
)

const(
    DefaultPort = 8888
    DefaultHost = `localhost`
    DefaultLogLevel = 1
)

type Runner struct {
    Port        int
    Host        string
    LogLevel    int
}

func NewRunner(options ...func(*Runner) error) (runner *Runner) {
    // any setup

    // set defaults
    runner = &Runner{DefaultPort, DefaultHost, DefaultLogLevel}

    for _, option := range options {
        err := option(runner)
        if err != nil {
            log.Fatalf("Failed to set NewRunner option: %s\n", err)
        }
    }
    return runner
}

func (r *Runner) Run() {
    fmt.Println(r)
}

func (r *Runner) String() string {
    return fmt.Sprintf("Runner Configuration:\n%16s %22d\n%16s %22s\n%16s %22d",
        `Port`, r.Port, `Host`, r.Host, `LogLevel`, r.LogLevel)
}

main:

package main

import(
    "errors"
    "flag"
    "github.com/jrefior/run" // update this path for your filesystem
)

func main() {
    // do something to get configuration values
    portFlag := flag.Int("p", 0, "Override default listen port")
    logLevelFlag := flag.Int("l", 0, "Override default log level")
    flag.Parse()

    // put your runner options here
    runnerOpts := make([]func(*run.Runner) error, 0)

    // with flags, we're not sure if port was set by flag, so test
    if *portFlag > 0 {
        runnerOpts = append(runnerOpts, func(runner *run.Runner) error {
            if *portFlag < 1024 {
                return errors.New("Ports below 1024 are privileged")
            }
            runner.Port = *portFlag
            return nil
        })
    }
    if *logLevelFlag > 0 {
        runnerOpts = append(runnerOpts, func(runner *run.Runner) error {
            if *logLevelFlag > 8 {
                return errors.New("The maximum log level is 8")
            }
            runner.LogLevel = *logLevelFlag
            return nil
        })
    }
    // other configuration if applicable
    runner := run.NewRunner(runnerOpts...)
    runner.Run()
}

示例用法:

$ ./program -p 8987
Runner Configuration:
            Port                   8987
            Host              localhost
        LogLevel                      1

【讨论】:

  • 非常感谢 1+,您能否提供一些我可以测试和使用的工作示例?喜欢你的第二个代码 sn-p ?play.golang.org
  • 我尝试了这段代码,但它不起作用...你能帮忙看看虚拟运行示例吗?
  • Go Playground 不是为您的问题编写/测试/运行解决方案的好地方,因为它不支持定义多个文件或多个包:所有内容都必须在 package main 中或从标准库 (list here)。您可以更新您的问题以附加不起作用的代码以便我可以提供帮助,或者您可以将其作为新问题发布。如果以后有机会,我也可以尝试编写一个可运行的示例,但它可能不会出现在 Go Playground 中。
  • 谢谢,如果你能分享你的working 代码,那就太好了
  • @JennyHilton 添加了一个可运行的示例
【解决方案2】:

我使用它来定义每个包的配置结构,这些结构更易于管理并在应用启动时加载。

像这样定义你的配置结构

type Config struct {
    Conf1               package1.Configuration        `group:"conf1"           namespace:"conf1"`
    Conf2               package2.Configuration        `group:"conf2"           namespace:"conf2"`
    Conf3               Config3                       `group:"conf3"           namespace:"conf3"`
    GeneralSetting      string                        `long:"Setting"          description:"setting"        env:"SETTING"      required:"true"`
}

type Config3 struct {
    setting string
}

并使用“github.com/jessevdk/go-flags”传递--config3.setting=stringValue cmd 参数或ENV 变量export CONFIG3_SETTING=stringValue

type Configuration interface {}

const DefaultFlags flags.Options = flags.HelpFlag | flags.PassDoubleDash

func Parse(cfg Configuration) []string {
    args, _ := flags.NewParser(cfg, DefaultFlags).Parse()

    return args
}

你的 main 应该是这样的:

func main() {
    // Parse the configuration.
    var cfg Config
    Parse(&cfg)
    service := NewService(cfg.Conf3.Setting)
}

【讨论】:

  • 谢谢,我想使用功能选项,如果您能提供我的上下文示例,那就太好了...
猜你喜欢
  • 1970-01-01
  • 2018-01-09
  • 2013-06-17
  • 2018-03-03
  • 1970-01-01
  • 2017-12-25
  • 1970-01-01
  • 2019-06-11
  • 2015-06-16
相关资源
最近更新 更多