Functional Options Pattern: 缺省传参的 Go 函数#
Created: May 4, 2021 2:32 PM
Tags: Golang
1. 问题#
在使用 Golang 的过程中,发现相对于 Python 它缺少一些语法糖,例如函数的参数可以有默认值,有默认值的参数可以不传。Java 虽然没有这种语法糖,但是他有重载可以实现类似的效果
exsample
def exsample(must_have, option_value = "world"):
print("{} {}".format(must_have + option_value))
exsample("hello")
output: hello world
exsample("hello", "friends")
output: hello friends
今天写 UT,需要构造数据,发现我们的构造数据方式有两种
- 直接使用 ORM 创建一个实例,参数值自定义
- 写一个 factory,用 faker 造一些数据,部分指定,部分使用 fake 的数据
这两个方法都有比较明显的缺陷
方法 1,每次都要构造所有的字段,很麻烦
方法 2,无法自定义实例的参数,在我们构造相关联的两个实例时,无法指定关联字段
2. 期望#
那么,有没有既可以最大限度的使用 faker 来造数据,同时又可以方便的自定某些参数的值进行关联呢?我们来看下 Python 里的 factory boy 是怎么做的
class ModelAFactory(factory.django.DjangoModelFactory):
class Meta:
model = ModelA
name = factory.Sequence(lambda n: "ModelA%s" % n)
description = factory.fuzzy.FuzzyText()
pic = factory.Sequence(lambda n: "image%s" % n)
status = Status.ACTIVE.value
@factory.post_generation
def related_models(obj, create, extracted, **kwargs):
RelatedModelFactory.create(model_a=obj, **kwargs)
ModelAFactory 在 meta 中定义了 Model,然后自定义了其中 4 个字段的生成方式,其他字段用默认方式生成。
使用时,一种是完全使用 factory 造的参数,另一种是可以指定部分字段的值。例子我举的是 name,更常见的情况是关联其他表的字段。
instance_a = ModelAFactory() # 完全依赖factory
instance_b = ModelAFactory(name="option") # 自定了instance name的值
3. 方案及实现#
3.1 背景知识#
Function types 函数类型 [1]#
- 自定义一个类型 (type),代表一系列类似的函数的集合 (func)
- 这些函数有相同的输入和输出
// 名字为TypeName的类型,是一系列函数的抽象类型,这些函数的共同特点是,
// 接收一个string参数,返回一个string参数
type TypeName func(var1 string) string
// 给TypeName类型添加一个方法say,所有TypeName的类型的函数都会有这个方法函数
func (typeName TypeName) say(n string) {
fmt.Println(typeName(n)) // 打印出某个TypeName类型的函数在传入参数n后的返回值
}
// 我们来实现一个TypeName类型的函数方法
func english(name string) string {
return "Hello, " + name
}
func french(name string) string {
return "Bonjour, " + name
}
func main() {
// 将english函数转换为TypeName类型,这样给english添加了一个方法say()
g := TypeName(english)
g.say("World")
g = Greeting(french)
g.say("World")
}
Closure 闭包#
-
概念
a closure is a record storing a function together with an environment.
闭包是由函数和与其相关的引用环境组合而成的实体 。- 函数是指外部函数和内部函数
- 外部函数主要是为了包裹环境,主要是为了构造一个带有环境的闭包函数
- 内部函数是实际需要执行函数,也就是闭包函数本身
- 闭包函数返回一个可执行的函数(内部函数)
- 环境
- 所谓环境,其实就是指外部函数传入的参数值,或者内部的参数,会被保存在闭包函数中,并且可以运用到内部函数的计算
- 同一个闭包函数,多次调用,会各自独立
这个概念在 Go 里面内容较多,这里只介绍足够我们实现当前功能的内容
- 函数是指外部函数和内部函数
-
闭包函数例子
func foo(x int) func() { return func(y int) { fmt.Printf("foo val = %d\n", x + y) } } // 初始化一个闭包函数, 1赋值给了foo的参数x,返回一个闭包函数(也就是带有x值的 func(y int)) f1 := foo(1) // 执行闭包函数, y=2, x=1, 执行func中的匿名函数 f1(2) // output:foo val = 3
Go 函数可变数量入参#
- Go 函数有变参数,即可以传入不定数量的同类型参数,go 的 print 函数就是利用这个特性。函数的入参可以是各种类型,包括自定类型,例如 function types
- 表示方式如下
// ...就表示有未数量的string参数可以传入
func test (v1 string, ...vs string) {
fmt.Println(v1)
for _, s := range vs {
fmt.Println(s)
}
}
使用起来,如下
test("A")
Out: "A"
test("A", "B", "C")
Out: "A"
"B"
"C"
3.2 方案#
Functional Options Pattern 函数选择模式 [2] [3]。
有了前面的知识,我们就可以来了解本文的主角 — 函数选择模式,它通过函数的不同参数的变化,修改或增加函数本身的功能。
3.2.1 例子#
h := NewHouse(
WithConcrete(),
WithoutFireplace()
)
NewHouse 在这里是一个构造函数,WithConcrete()
和WithFireplace()
是构造函数的可选参数,这些参数可以修改函数的返回值。
3.2.2 定义构造函数#
// 定义一个结构体,里面存放我们可选参数的值
type User struct {
Name string
}
// 构造函数, 默认名为张三
func NewUser() User {
options := User{}
options.Name = "张三"
return options
}
3.2.3 定义函数选项#
外部函数 WithName 传入 Name 字段对应想要自定义的值 name,返回一个 UserOption 类型的函数,也就是 return 的那个匿名函数。函数的作用是将 name 的参数值复制到,传入可选参数结构体的指针的对应字段 Name,这样赋值后,函数外的 * User 值也会修改。
// 定义一个自定义的函数类型,作为闭包函数的返回
// 入参为可变参数struct的指针,因为需要修改闭包外的值作为存储
type UserOption func(options *User)
// 创建一个选项函数
func WithName(name string) UserOption {
return func(options *User) {
options.Name = name
}
}
3.2.4 将函数选项添加到构造函数#
此时我们需要修改一下之前的构造函数,让其支持传入多个函数选项。"...UserOption" 表示可以传入未知数量的 UserOption 类型的函数
func NewUser(opts ...UserOption) User {
options := User{}
options.Name = "张三"
// 循环执行选项函数,进行字段的赋值
for _, o := range opts {
o(&options)
}
return options
}
使用,注意下,WithName 这个函数其实用闭包生成一个带有可赋值参数值的 UserOption 类型函数,然后构造函数只要将存储值的 struct 传入执行一下,就可以把里面的值替换掉
func main() {
fmt.Println("Hello, playground")
// 使用默认值
user := NewUser()
fmt.Println("Name", user.Name)
// 使用自定义值
user2 := NewUser(WithName("黄子龙"))
fmt.Println("Name", user2.Name)
}
4. Show me the code!#
// wechat 相关.
// 定义一个类型,可以是函数,传入值是我们的model,因为使用闭包保证赋值,所以没有传出.
type WechatOption func(options *entschema.WeChat)
// 用闭包进行自定义赋值.
func WithAppIDWechat(s string) WechatOption {
return func(options *entschema.WeChat) {
options.AppID = s
}
}
func WithUnionIDWechat(s string) WechatOption {
return func(options *entschema.WeChat) {
options.UnionID = &s
}
}
func WithOpenIDWechat(s string) WechatOption {
return func(options *entschema.WeChat) {
options.OpenID = s
}
}
func WithAccountIDWechat(s uint64) WechatOption {
return func(options *entschema.WeChat) {
options.AccountID = s
}
}
func WithTypeWechat(s wechat.Type) WechatOption {
return func(options *entschema.WeChat) {
options.Type = s
}
}
func WeChatFactory(suite TestSuite, opts ...WechatOption) entschema.WeChat {
options := entschema.WeChat{}
// 配置默认的fake数据, fake参数在factory内.
suite.NoError(faker.FakeData(&options))
// 传参则使用传入的参数进行赋值,实现可变传参.
for _, o := range opts {
o(&options)
}
aWechat := app.Database.WeChat.Create().
SetAppID(options.AppID).
SetOpenID(options.OpenID).
SetAccountID(options.AccountID).
SetUnionID(*options.UnionID).
SetType(options.Type).
SaveX(suite.Context())
return *aWechat
}
wechatFactory(WithUnionIDWechat("id"))
5. One More Thing#
虽然我们学习了这个知识,也确实知道这个东西很不错,但不代表我们会去用。我注意到其实我们不少小伙伴其实有在一些地方用到,但并非所有需要的地方都用到了。
究其原因,还是因为这太麻烦了,要写很多代码,相比于 python 只要定一个参数名,再设置一个默认值的方式。Go 需要实现同样的功能要写多得多的代码,很明显,我们没有这个时间。它也不值得我们花时间这些时间。这算是性能的代价吧。但有没有双全法呢?有的
Generate-function-opts Command#
./generate-function-opts
-definitionFile gen/entschema/workwechatuser.go
-structTypeName WorkWeChatUser
-outFile pkg/testutils/workwechatuserfactory.go
-skipStructFields Edges
reference#
- Golang.org. 2021. The Go Programming Language Specification - The Go Programming Language. [online] Available at: https://golang.org/ref/spec#Function_types [Accessed 25 February 2021].
- talks, C., 2018. Go 函数选项模式. [online] Code talks. Available at: https://lingchao.xin/post/functional-options-pattern-in-go.html [Accessed 25 February 2021].
- Sohamkamani.com. 2019. [online] Available at: https://www.sohamkamani.com/golang/options-pattern/ [Accessed 26 February 2021].