lestrrat-go/option

GitHub: lestrrat-go/option

为 Go 语言提供可复用的 Optional Parameter 模式基础类型,帮助库作者以更优雅的签名设计支持可选参数。

Stars: 9 | Forks: 5

# 选项 “可选参数模式”的基础对象。 # 描述 这种模式的精妙之处在于,你可以实现一个支持以下简单调用风格的方法 ``` obj.Method(mandatory1, mandatory2) ``` 或者,如果你想通过可选参数来修改其行为,可以采用以下方式 ``` obj.Method(mandatory1, mandatory2, optional1, optional2, optional3) ``` 从而避免了更为笨拙的、为可选参数设置零值的风格 ``` obj.Method(mandatory1, mandatory2, nil, "", 0) ``` 或是同样笨拙的配置对象风格,后者要求你创建一个带有 `NamesThatLookReallyLongBecauseItNeedsToIncludeMethodNamesConfig` 的结构体 ``` cfg := &ConfigForMethod{ Optional1: ..., Optional2: ..., Optional3: ..., } obj.Method(mandatory1, mandatory2, &cfg) ``` # 概要 为 option 创建一个“标识符”。我们建议使用未导出的空结构体,因为: 1. 它在全局范围内具有唯一可识别性 2. 占用最小的空间 3. 由于它是未导出的,你不必担心它会泄露到其他地方或被使用者修改 ``` // an unexported empty struct type identFeatureX struct{} ``` 然后定义一个方法,使用此标识符来创建一个 option。这里我们假设该 option 将是一个布尔型 option。 ``` // this is optional, but for readability we usually use a wrapper // around option.Interface, or a type alias. type Option func WithFeatureX(v bool) Option { // use the constructor to create a new option return option.New(identFeatureX{}, v) } ``` 现在你可以创建一个 option,它本质上是一个由标识符及其关联值组成的二元元组。 要使用它,你需要创建一个带有可变参数的函数,并遍历列表以查找特定的标识符: ``` func MyAwesomeFunc( /* mandatory parameters omitted */, options ...[]Option) { var enableFeatureX bool // The nolint directive is recommended if you are using linters such // as golangci-lint //nolint:forcetypeassert for _, option := range options { switch option.Ident() { case identFeatureX{}: enableFeatureX = option.Value().(bool) // other cases omitted } } if enableFeatureX { .... } } ``` # Option 对象 Option 对象接受两个参数:其标识符和它所包含的值。 标识符可以是任何东西,但通常最好使用未导出的空结构体,这样只有你才有能力生成该 option: ``` type identOptionalParamOne struct{} type identOptionalParamTwo struct{} type identOptionalParamThree struct{} func WithOptionOne(v ...) Option { return option.New(identOptionalParamOne{}, v) } ``` 然后你可以像上面描述的那样调用该方法 ``` obj.Method(m1, m2, WithOptionOne(...), WithOptionTwo(...), WithOptionThree(...)) ``` Option 的解析应该在类似于以下代码的结构中进行 ``` func (obj *Object) Method(m1 Type1, m2 Type2, options ...Option) { paramOne := defaultValueParamOne for _, option := range options { switch option.Ident() { case identOptionalParamOne{}: paramOne = option.Value().(...) } } ... } ``` 这个循环需要一些样板代码,不可否认,这是该模块的主要缺点。然而,如果你认为你想使用 Option 作为 Function 模式,请查看下面的 FAQ 了解原因。 # 简单用法 大多数情况下,你需要做的只是在代码中将 Option 类型声明为别名: ``` package myawesomepkg import "github.com/lestrrat-go/option" type Option = option.Interface ``` 然后你就可以像“概要”部分中描述的那样开始定义 option 了。 # 区分 Options 当你有多个方法和 option,且这些 option 只能分别传递给对应的方法时,很难一眼看出哪些 option 应该传递给哪个方法。 ``` func WithX() Option { ... } func WithY() Option { ... } // Now, which of WithX/WithY go to which method? func (*Obj) Method1(options ...Option) {} func (*Obj) Method2(options ...Option) {} ``` 在这种情况下,最简单的明确方式是在这些 option 外层再包一层,使它们具有不同的类型 ``` type Method1Option interface { Option method1Option() } type method1Option struct { Option } func (*method1Option) method1Option() {} func WithX() Method1Option { return &methodOption{option.New(...)} } func (*Obj) Method1(options ...Method1Option) {} ``` 这样一来,编译器就能知道某个 option 是否可以传递给特定的方法。 # 常见问题 ## 为什么这些不是基于函数的? 使用像 `type Option func(ctx interface{})` 这样的基础 option 类型肯定是实现同一目标的一种方式。在这种情况下,你赋予了 option 本身去“配置”主对象的能力。例如: ``` type Foo struct { optionaValue bool } type Option func(*Foo) error func WithOptionalValue(v bool) Option { return Option(func(f *Foo) error { f.optionalValue = v return nil }) } func NewFoo(options ...Option) (*Foo, error) { var f Foo for _, o := range options { if err := o(&f); err != nil { return nil, err } } return &f } ``` 这本身没什么问题,但我们认为存在几个隐患: ### 1. 很难创建可重用的“Option”类型 我们使用这种可选模式创建了许多库。我们希望提供一个默认的基础对象。然而,这种基于函数的方法是不可重用的,因为每个“Option”类型都要求它具有特定于上下文的输入类型。例如,如果上例中的“Option”类型是 `func(interface{}) error`,那么由于类型转换,其可用性将大幅降低。 这并不是说本库的方法更好,因为它同样需要进行类型转换来转换 option 的 _值_。然而,最初基于函数的方法的精妙之处部分在于其易用性,而我们主张上述情况极大地削弱了基于函数方法的优点。 ### 2. 接收者需要导出的字段 基于函数的 option 模式的一部分吸引力在于:通过赋予 option 本身执行其所需操作的能力,你为第三方创造了可能性,使他们能够创建出库作者未曾考虑到的功能的 option。 ``` package thirdparty , but when I read drum sheet music, I kind of get thrown off b/c many times it says to hit the bass drum where I feel like it's a snare hit. func WithMyAwesomeOption( ... ) mypkg.Option { return mypkg.Option(func(f *mypkg) error { f.X = ... f.Y = ... f.Z = ... return nil }) } ``` 然而,要让任何第三方代码访问并设置字段值,这些字段(`X`、`Y`、`Z`)必须是被导出的。基本上,你需要一个“开放”的结构体。 当一个结构体仅仅代表数据时(例如,在涉及引用或更改状态信息的 API 调用时),导出字段完全没问题,但我们认为随意为库结构体暴露字段是未来陷入维护地狱的必经之路。当你想更改 API 时会发生什么?当你意识到你想将该字段用作状态(即不仅用于配置)时会发生什么?如果他们一直引用该字段,然后你的并发代码又去访问它,该怎么办? 赋予第三方对导出字段的完全访问权限,就像是把上了膛的武器交给了用户,你只能任凭他们摆布。 当然,为所有内容提供公共 API 以便你可以验证和控制并发也是一种选择,但是……这需要大量的工作,而且你可能 _仅仅_ 为了让用户在 option 配置阶段能够引用它,而不得不提供 API。这听起来像是很多额外的工作。
标签:EVTX分析, 日志审计