Go 語言開源項目使用的函數選項模式

大家好,我是 frank。
歡迎大家關注「Golang 語言開發棧」公衆號。

01 介紹

在閱讀 Go 語言開源項目的源碼時,我們可以發現有很多使用 “函數選項模式”  的代碼,“函數選項模式” 是 Rob Pike 在 2014 年提出的一種模式,它使用 Go 語言的兩大特性,變長參數和閉包,可以使我們代碼更優雅。

關於變長參數和閉包的介紹,需要的讀者朋友們可以查閱歷史文章,本文我們介紹 “函數選項模式” 的相關內容。

02 使用方式

在介紹 “函數選項模式” 的使用方式之前,我們先閱讀以下這段代碼。

type User struct {
    Id int
    Name string
}

type option func(*User)

func (u *User) Option(opts ...option) {
    for _, opt := range opts {
        opt(u)
    }
}

func WithId(id int) option {
    return func(u *User) {
        u.Id = id
    }
}

func WithName(name string) option {
    return func(u *User) {
        u.Name = name
    }
}

func main() {
    u1 := &User{}
    u1.Option(WithId(1))
    fmt.Printf("%+v\n", u1)
    
    u2 := &User{}
    u2.Option(WithId(1), WithName("frank"))
    fmt.Printf("%+v\n", u2)
}

輸出結果:

&{Id:1 Name:}
&{Id:1 Name:frank}

閱讀上面這段代碼,我們可以發現,首先,我們定義一個名字是 option 的類型,它實際上是一個可以接收一個參數的函數。

然後,我們給 User 結構體定義一個 Option 方法,該方法接收我們定義的 option 類型的變長參數,方法體中使用 for-loop 執行函數。

定義 WithId 函數和 WithName 函數,設置 User 結構體的字段 Id 和字段 Name,該函數通過返回閉包的形式實現。

以上使用方式是 “函數選項模式” 的一般使用方式。該使用方式可以解決大部分問題,但是,“函數選項模式” 還有進階使用方式,感興趣的讀者朋友們可以繼續閱讀 Part 03 的內容。

03 進階使用方式

所謂 “函數選項模式” 的進階使用方式,即有返回值的 “函數選項模式”,其中,返回值包含 golang 內置類型和自定義 option 類型。

內置類型的返回值

type User struct {
    Id int
    Name string
}

type option func(*User) interface{}

func (u *User) Option(opts ...option) (id interface{}) {
    for _, opt := range(opts) {
        id = opt(u)
    }
    return id
}

func WithId(id int) option {
 return func(u *User) interface{} {
  prevId := u.Id
  u.Id = id
  return prevId
 }
}

func main () {
    u1 := &User{Id: 1}
    id := u1.Option(WithId(2))
    fmt.Println(id.(int))
    fmt.Printf("%+v\n", u1)
}

輸出結果:

1
&{Id:2 Name:}

閱讀上面這段代碼,我們在定義 option 類型時,使用一個有返回值函數(此處使用的是空接口類型的返回值)。

WithId 函數的函數體中的代碼也稍作修改,閉包中使用 prevId 變量存儲結構體 User 字段 Id 的原始數據,並作爲函數返回值。

細心的讀者朋友們可能已經發現,我們在 main 函數中顯式處理返回值,即:

...
id := u1.Option(WithId(2))
fmt.Println(id.(int))
...

如果我們想要避免顯式處理返回值,可以使用返回自定義 option 類型的返回值的形式。

自定義 option 類型的返回值

type User struct {
    Id int
    Name string
}

type option func(*User) option

func (u *User) Option(opts ...option) (prev option) {
    for _, opt := range opts {
        prev = opt(u)
    }
    return prev
}

func WithId(id int) option {
    return func(u *User) option {
        prevId := u.Id
        u.Id = id
        return WithId(prevId)
    }
}

func main () {
    u1 := &User{Id: 1}
    prev := u1.Option(WithId(2))
    fmt.Printf("%+v\n", u1)
    u1.Option(prev)
    fmt.Printf("%+v\n", u1)
}

輸出結果:

&{Id:2 Name:}
&{Id:1 Name:}

閱讀上面這段代碼,我們在定義 option 類型時,通過把函數的返回值更改爲 option 類型,我們就可以在 WithId 函數中,使用閉包處理 User 結構體 Id 字段的原始值。

需要注意的是, User 結構體 Option 方法的返回值是  option 類型。

04 使用示例

我們在瞭解完 “函數選項模式” 之後,使用該模式實現一個簡單示例。

type User struct {
    Id int
    Name string
    Email string
}

type option func(*User)

func WithId(id int) option {
    return func(u *User) {
        u.Id = id
    }
}

func WithName(name string) option {
    return func(u *User) {
        u.Name = name
    }
}

func WithEmail(email string) option {
 return func(u *User) {
  u.Email = email
 }
}

func NewUser(opts ...option) *User {
    const (
        defaultId = -1
        defaultName = "guest"
        defaultEmail = "undefined"
    )
    u := &User{
        Id: defaultId,
        Name: defaultName,
        Email: defaultEmail,
    }
    
    for _, opt := range opts {
        opt(u)
    }
    return u
}

func main() {
    u1 := NewUser(WithName("frank"), WithId(1000000001))
    fmt.Printf("%+v\n", u1)
    u2 := NewUser(WithEmail("gopher@88.com"))
    fmt.Printf("%+v\n", u2)
    u3 := NewUser()
    fmt.Printf("%+v\n", u3)
}

輸出結果:

&{Id:1000000001 Name:frank Email:undefined}
&{Id:-1 Name:guest Email:gopher@88.com}
&{Id:-1 Name:guest Email:undefined}

閱讀上面這段代碼,我們使用 “函數選項模式” 實現構造函數 NewUser,不僅可以自定義默認值(避免使用 Go 類型零值作爲默認值),而且還可以使調用者靈活傳參(無需關心參數的順序和個數)。

05 總結

本文我們介紹怎麼使用 Go 語言的 “函數選項模式”,通過閱讀完本文所有內容,讀者朋友們應該已經感受到該模式的優點。

但是,該模式也有缺點,比如需要定義 WithXxx 函數,增加了代碼量。

所以,我們可以根據實際使用場景決定是否選擇使用 “函數選項模式”。

參考資料:

  1. https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html

  2. https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2jzg2PIK_esjTxSFMkp02A