功能選項模式:缺省傳參的 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].