深圳幻海软件技术有限公司 欢迎您!

Go 语言开源项目使用的函数选项模式

2023-02-28

​1.介绍在阅读Go语言开源项目的源码时,我们可以发现有很多使用“函数选项模式” 的代码,“函数选项模式”是RobPike在2014年提出的一种模式,它使用 Go语言的两大特性,变长参数和闭包,可以使我们代码更优雅。关于变长参数和闭包的介绍,需要的读者朋友们可以查阅历史文章,本文

​1.介绍

在阅读 Go 语言开源项目的源码时,我们可以发现有很多使用 “函数选项模式”  的代码,“函数选项模式” 是 Rob Pike 在 2014 年提出的一种模式,它使用 Go 语言的两大特性,变长参数和闭包,可以使我们代码更优雅。

关于变长参数和闭包的介绍,需要的读者朋友们可以查阅历史文章,本文我们介绍 “函数选项模式” 的相关内容。

2.使用方式

在介绍“函数选项模式”的使用方式之前,我们先阅读以下这段代码。

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)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

输出结果:

&{Id:1 Name:}
&{Id:1 Name:frank}
  • 1.
  • 2.

阅读上面这段代码,我们可以发现,首先,我们定义一个名字是 option 的类型,它实际上是一个可以接收一个参数的函数。

然后,我们给 User​ 结构体定义一个 Option​ 方法,该方法接收我们定义的 option​ 类型的变长参数,方法体中使用 for-loop 执行函数。

定义 WithId​ 函数和 WithName​ 函数,设置 User​ 结构体的字段 Id​ 和字段 Name,该函数通过返回闭包的形式实现。

以上使用方式是 “函数选项模式” 的一般使用方式。该使用方式可以解决大部分问题,但是,“函数选项模式” 还有进阶使用方式,感兴趣的读者朋友们可以继续阅读 Part 03 的内容。

3.进阶使用方式

所谓 “函数选项模式” 的进阶使用方式,即有返回值的 “函数选项模式”,其中,返回值包含 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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

输出结果:

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

阅读上面这段代码,我们在定义 option 类型时,使用一个有返回值函数(此处使用的是空接口类型的返回值)。

WithId​ 函数的函数体中的代码也稍作修改,闭包中使用 prevId​ 变量存储结构体 User​ 字段 Id 的原始数据,并作为函数返回值。

细心的读者朋友们可能已经发现,我们在 main 函数中显式处理返回值,即:

...
id := u1.Option(WithId(2))
fmt.Println(id.(int))
...
  • 1.
  • 2.
  • 3.
  • 4.

如果我们想要避免显式处理返回值,可以使用返回自定义 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)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

输出结果:

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

阅读上面这段代码,我们在定义 option​ 类型时,通过把函数的返回值更改为 option​ 类型,我们就可以在 WithId​ 函数中,使用闭包处理 User​ 结构体 Id 字段的原始值。

需要注意的是, User​ 结构体 Option​ 方法的返回值是  option 类型。

4.使用示例

我们在了解完 “函数选项模式” 之后,使用该模式实现一个简单示例。

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)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.

输出结果:

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

阅读上面这段代码,我们使用 “函数选项模式” 实现构造函数 NewUser,不仅可以自定义默认值(避免使用 Go 类型零值作为默认值),而且还可以使调用者灵活传参(无需关心参数的顺序和个数)。

5.总结

本文我们介绍怎么使用 Go 语言的 “函数选项模式”,通过阅读完本文所有内容,读者朋友们应该已经感受到该模式的优点。

但是,该模式也有缺点,比如需要定义 WithXxx 函数,增加了代码量。

所以,我们可以根据实际使用场景决定是否选择使用 “函数选项模式”。