• Go 学习笔记(87) — 函数式选项,初始化结构体对象可变参数


    当我们遇到一定要初始化一个类的时候,大部分时候,我们都会使用类似下列的 New 方法:

    
    package newdemo
    
    type Foo struct {
       name string
       id int
       age int
    
       db interface{}
    }
    
    func NewFoo(name string, id int, age int, db interface{}) *Foo {
       return &Foo{
          name: name,
          id:   id,
          age:  age,
          db:   db,
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这段代码中,我们定义一个 NewFoo 方法,其中存放初始化 Foo 结构所需要的各种字段属性。

    这个写法乍看之下是没啥问题的,但是一旦 Foo 结构内部的字段发生了变化,增加或者减少了,那么这个初始化函数 NewFoo 就怎么看怎么别扭了。

    参数继续增加?那么所有调用了这个 NewFoo 方法的地方也都需要进行修改,且按照代码整洁的逻辑,参数多于 5 个,这个函数就很难使用了。而且,如果这 5 个参数都是可有可无的参数,就是有的参数可以不填写,有默认值,比如 age 这个字段,即使我们不填写,在后续的业务逻辑中可能也没有很多影响,那么我在实际调用 NewFoo 的时候,age 这个字段还需要传递 0 值:

    foo := NewFoo("wohu", 1, 0, nil)
    
    • 1

    这里其实有一种更好的写法:使用 Option 写法来进行改造。

    Option 写法,顾名思义,就是将所有可选的参数作为一个可选方式,一般我们会设计一个“函数类型”来代表这个 Option,然后配套将所有可选字段设计为一个这个函数类型的具体实现。在具体的使用的时候,使用可变字段的方式来控制有多少个函数类型会被执行。比如上述的代码,我们会改造为:

    
    type Foo struct {
        name string
        id int
        age int
    
        db interface{}
    }
    
    // FooOption 代表可选参数
    type FooOption func(foo *Foo)
    
    // WithName 代表Name为可选参数
    func WithName(name string) FooOption {
       return func(foo *Foo) {
          foo.name = name
       }
    }
    
    // WithAge 代表age为可选参数
    func WithAge(age int) FooOption {
       return func(foo *Foo) {
          foo.age = age
       }
    }
    
    // WithDB 代表db为可选参数
    func WithDB(db interface{}) FooOption {
       return func(foo *Foo) {
          foo.db = db
       }
    }
    
    // NewFoo 代表初始化
    func NewFoo(id int, options ...FooOption) *Foo {
       foo := &Foo{
          name: "default",
          id:   id,
          age:  10,
          db:   nil,
       }
       for _, option := range options {
          option(foo)
       }
       return foo
    }
    
    • 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

    现在我们来解释下上面的这段代码,我们创建了一个 FooOption 的函数类型,这个函数类型代表的函数结构是 func(foo *Foo) 。这个结构很简单,就是将 foo 指针传递进去,能让内部函数进行修改。

    然后我们针对三个初始化字段 nameagedb 定义了三个返回了 FooOption 的函数,负责修改它们:WithNameWithAgeWithDB

    WithName 为例,这个函数参数为 string,返回值为 FooOption。在返回值的 FooOption 中,根据参数修改了 Foo 指针。

    // WithName 代表Name为可选参数
    func WithName(name string) FooOption {
       return func(foo *Foo) {
          foo.name = name
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    顺便说一下,这种函数我们一般都以 With 开头,表示我这次初始化带着这个字段。而最后 NewFoo 函数的参数,我们就改造为两个部分:

    • 一个部分是非 Option 字段,就是必填字段,假设我们的 Foo 结构实际上只有一个必填字段 id,而其他字段皆是选填的;
    • 第二个部分就是其他所有选填字段,我们使用一个可变参数 options 替换;
    NewFoo(id int, options ...FooOption)
    
    • 1

    在具体的 NewFoo 实现中,也变化成 2 个步骤:
    按照默认值初始化一个 foo 对象;遍历 options 改造这个 foo 对象。

    // 具体使用NewFoo的函数
    func Bar() {
       foo := NewFoo(1, WithAge(15), WithName("foo"))
       fmt.Println(foo)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果我们后续 Foo 多了一个可变属性,那么我们只需要多一个 WithXXX 的方法就可以了,而 NewFoo 函数不需要任何变化,调用方只要在指定这个可变属性的地方增加 WithXXX 就可以了,扩展性非常好。

  • 相关阅读:
    网络工程师回顾学习(第三部分)
    Spark Friendship
    chrome extension 普通网页与插件直接通信
    arcgis实现矢量数据的局部裁剪
    能解决你80%关于存储的疑惑
    【历史上的今天】8 月 3 日:微软研究院的创始人诞生;陌陌正式上线;苹果发布 Newton OS
    c++的priority_queue各种使用方法
    25 个超棒的 Python 脚本合集
    函数栈详解
    docker容器常用命令
  • 原文地址:https://blog.csdn.net/wohu1104/article/details/123004871