具有不同配置道具的不同包 - 功能选项

Jen*_*ton 9 go

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

在努力找到好方法之后我发现了这篇文章(有点老但希望仍然相关),我想知道如何以及如果我可以用它来解决我的问题.

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

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

例如对于函数run(这是入口点)我需要注入log level和其他一些env变量一样port host

对于功能build我需要"注入"的build flavorbuild type等.

我的内容的任何示例都将非常有用

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

更新

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

jre*_*ior 4

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

如果您的应用程序的配置是静态的,因为它不太可能通过不同的执行线程更改(变异),并且您不需要在同一个中创建具有不同配置的多个实例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
}
Run Code Online (Sandbox Code Playgroud)

但这并不能解决戴夫·切尼帖子中提到的问题。如果用户在没有主机、端口或 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)
    // ...
Run Code Online (Sandbox Code Playgroud)

在某种程度上,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
Run Code Online (Sandbox Code Playgroud)

这是更传统的方法,对于大多数开发人员来说可能更容易阅读和理解,如果它适合您的需求,那么这也是一种完美的方法。(如果需要,您可以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)
}
Run Code Online (Sandbox Code Playgroud)

包裹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()
}
Run Code Online (Sandbox Code Playgroud)

用法示例:

$ ./program -p 8987
Runner Configuration:
            Port                   8987
            Host              localhost
        LogLevel                      1
Run Code Online (Sandbox Code Playgroud)