「读书笔记」了解变长参数函数的妙用

144 阅读6分钟

函数与方法

25 了解变长参数函数的妙用

变长参数函数

变长参数函数指调用时可以接受零个、一个或多个实际参数的函数,如func fmt.Println(a ...interface{}) (n int, err error)

一个变长参数函数只能有一个 "...T" 类型形式参数,并且该形式参数应该为函数参数列表中的最后一个形式参数,否则 Go 编译器就会报错。

变长参数函数的 "...T" 类型形式参数在函数体内呈现为 []T 类型的变量,我们可以将其理解为一个 Go 语法糖。

在函数外部,"...T" 类型形式参数可匹配和接受的实参类型有两种:

  • 多个 T 类型变量;
  • t...(t 为 []T 类型变量);

但是将两种混用会得到一个编译错误。

还要注意:虽然 string 类型变量可以直接赋值给 interface{} 类型变量,但是 []string 类型变量并不能直接赋值给 []interface{} 类型变量。如下所示:

func dump(args ...interface{}) {
   for _, v := range args {
      fmt.Println(v)
   }
}

func main() {
   // s := []string{"Tony", "John", "Jim"}   // 编译错误
   s := []interface{}{"Tony", "John", "Jim"} // 正常运行
   dump(s...)
}

模拟函数重载

Go 语言不允许在同一个作用域下定义名字相同但函数原型不同的函数,即不支持重载函数。官方给出的不支持理由是:

其他语言的经验告诉我们,使用具有相同名称但函数签名不同的多种方法有时会很有用,但在实践中也可能会造成混淆和脆弱性。在 Go 的类型系统中仅按名称进行匹配,并要求类型一致是一个主要的简化决策。

在 Go 语言中我们可以通过变长参数函数来模拟重载函数

  • 如果要重载的函数的参数都是相同类型的,仅参数的个数是变化的,那么变长参数函数可以轻松应对。
  • 如果参数类型不同且个数可变,那么我们还要结合interface类型的特性。如下所示:
func concat(sep string, args ...interface{}) string {
   var result string
   for i, v := range args {
      if i != 0 {
         result += sep
      }
      switch v.(type) {
      case int, int8, int16, int32, int64,
         uint, uint8, uint16, uint32, uint64:
         result += fmt.Sprintf("%d", v)
      case string:
         result += fmt.Sprintf("%s", v)
      case []int:
         ints := v.([]int)
         for i, v := range ints {
            if i != 0 {
               result += sep
            }
            result += fmt.Sprintf("%d", v)
         }
      case []string:
         strs := v.([]string)
         result += strings.Join(strs, sep)
      default:
         fmt.Printf("the argument type [%T] is not supported", v)
         return ""
      }
   }
   return result
}

func main() {
   println(concat("-", 1, 2))              // 1-2
   println(concat("-", "hello", "gopher")) // hello-gopher
   println(concat("-", "hello", 1, uint32(2),
      []int{11, 12, 13}, 17,
      []string{"robot", "ai", "ml"},
      "hacker", 33)) // hello-1-2-11-12-13-17-robot-ai-ml-hacker-33
}

模拟实现函数的可选参数与默认参数

如果参数在传入时有隐式要求的固定顺序(这点由调用者保证),我们还可以利用变长参数函数模拟实现函数的可选参数和默认参数。如下所示:

type record struct {
   name    string
   gender  string
   age     uint16
   city    string
   country string
}

func enroll(args ...interface{} /* name, gender, age, city = "Beijing", country = "China" */) (*record, error) {
   if len(args) > 5 || len(args) < 3 {
      return nil, fmt.Errorf("the number of arguments passed is wrong")
   }

   r := &record{
      city:    "Beijing", // 默认值:Beijing
      country: "China",   // 默认值:China
   }

   for i, v := range args {
      switch i {
      case 0: // name
         name, ok := v.(string)
         if !ok {
            return nil, fmt.Errorf("name is not passed as string")
         }
         r.name = name
      case 1: // gender
         gender, ok := v.(string)
         if !ok {
            return nil, fmt.Errorf("gender is not passed as string")
         }
         r.gender = gender
      case 2: // age
         age, ok := v.(int)
         if !ok {
            return nil, fmt.Errorf("age is not passed as int")
         }
         r.age = uint16(age)
      case 3: // city
         city, ok := v.(string)
         if !ok {
            return nil, fmt.Errorf("city is not passed as string")
         }
         r.city = city
      case 4: // country
         country, ok := v.(string)
         if !ok {
            return nil, fmt.Errorf("country is not passed as string")
         }
         r.country = country
      default:
         return nil, fmt.Errorf("unknown argument passed")
      }
   }

   return r, nil
}

func main() {
   r, _ := enroll("小明", "male", 23)
   fmt.Printf("%+v\n", *r) // {name:小明 gender:male age:23 city:Beijing country:China}

   r, _ = enroll("小红", "female", 13, "Hangzhou")
   fmt.Printf("%+v\n", *r) // {name:小红 gender:female age:13 city:Hangzhou country:China}

   r, _ = enroll("Leo Messi", "male", 33, "Barcelona", "Spain")
   fmt.Printf("%+v\n", *r) // {name:Leo Messi gender:male age:33 city:Barcelona country:Spain}

   r, err := enroll("小吴", 21, "Suzhou")
   if err != nil {
      fmt.Println(err) // gender is not passed as string
      return
   }
}

实现功能选项模式

在日常 Go 编程中,我们经常会实现一些带有设置选项的创建型函数。比如:我们要创建一个网络通信的客户端,创建客户端实例的函数需要提供某种方式以让调用者设置客户端的一些行为属性,如超时时间,重置次数等。对于一些复杂的 Go 包中的创建型函数,他要提供的可设置选项有时多达数十种,甚至后续还会增加,因此设计和实现这样的创建型函数时要尤为注意、考虑使用者的体验,不能因选项过多而提供过多的API,并且要保证选项持续增加后,函数的对外接口依旧保持稳定。

实现方法如下:

  • 通过参数暴露配置选项:无法扩展。

    type FinishedHouse struct {
       style                  int    // 0: Chinese, 1: American, 2: European
       centralAirConditioning bool   // true or false
       floorMaterial          string // "ground-tile" or ”wood"
       wallMaterial           string // "latex" or "paper" or "diatom-mud"
    }
    
    func NewFinishedHouse(style int, centralAirConditioning bool,
       floorMaterial, wallMaterial string) *FinishedHouse {
    
       // here: you should do some check to the arguments passed
    
       h := &FinishedHouse{
          style:                  style,
          centralAirConditioning: centralAirConditioning,
          floorMaterial:          floorMaterial,
          wallMaterial:           wallMaterial,
       }
    
       return h
    }
    
    func main() {
       fmt.Printf("%+v\n", NewFinishedHouse(0, true, "wood", "paper"))
    }
    
  • 使用结构体封装配置选项:需要显式配置所有字段;options 字段在函数调用后还可以变化。

    type FinishedHouse struct {
       style                  int    // 0: Chinese, 1: American, 2: European
       centralAirConditioning bool   // true or false
       floorMaterial          string // "ground-tile" or ”wood"
       wallMaterial           string // "latex" or "paper" or "diatom-mud"
    }
    
    type Options struct {
       Style                  int    // 0: Chinese, 1: American, 2: European
       CentralAirConditioning bool   // true or false
       FloorMaterial          string // "ground-tile" or ”wood"
       WallMaterial           string // "latex" or "paper" or "diatom-mud"
    }
    
    func NewFinishedHouse(options *Options) *FinishedHouse {
       // use default style and materials if option is nil
       var style int = 0
       var centralAirConditioning = true
       var floorMaterial = "wood"
       var wallMaterial = "paper"
    
       if options != nil {
          // here: you should do some check to the options passed
    
          style = options.Style
          centralAirConditioning = options.CentralAirConditioning
          floorMaterial = options.FloorMaterial
          wallMaterial = options.WallMaterial
       }
    
       h := &FinishedHouse{
          style:                  style,
          centralAirConditioning: centralAirConditioning,
          floorMaterial:          floorMaterial,
          wallMaterial:           wallMaterial,
       }
    
       return h
    }
    
    func main() {
       fmt.Printf("%+v\n", NewFinishedHouse(nil)) // use default options
       fmt.Printf("%+v\n", NewFinishedHouse(&Options{
          Style:                  1,
          CentralAirConditioning: false,
          FloorMaterial:          "ground-tile",
          WallMaterial:           "paper",
       }))
    }
    
  • 使用功能选项模式:Go 语言之父 Rob Pike 早在 2014 年就在其博文“自引用函数与选项设计”中论述了一种被后人称为“功能选项”(function option)的模式,这种模式应该是目前进行功能选项设计的最佳实践。

    type FinishedHouse struct {
       style                  int    // 0: Chinese, 1: American, 2: European
       centralAirConditioning bool   // true or false
       floorMaterial          string // "ground-tile" or ”wood"
       wallMaterial           string // "latex" or "paper" or "diatom-mud"
    }
    
    type Option func(*FinishedHouse)
    
    func NewFinishedHouse(options ...Option) *FinishedHouse {
       h := &FinishedHouse{
          // default options
          style:                  0,
          centralAirConditioning: true,
          floorMaterial:          "wood",
          wallMaterial:           "paper",
       }
    
       for _, option := range options {
          option(h)
       }
    
       return h
    }
    
    func WithStyle(style int) Option {
       return func(h *FinishedHouse) {
          h.style = style
       }
    }
    
    func WithFloorMaterial(material string) Option {
       return func(h *FinishedHouse) {
          h.floorMaterial = material
       }
    }
    
    func WithWallMaterial(material string) Option {
       return func(h *FinishedHouse) {
          h.wallMaterial = material
       }
    }
    
    func WithCentralAirConditioning(centralAirConditioning bool) Option {
       return func(h *FinishedHouse) {
          h.centralAirConditioning = centralAirConditioning
       }
    }
    
    func main() {
       fmt.Printf("%+v\n", NewFinishedHouse()) // use default options
       fmt.Printf("%+v\n", NewFinishedHouse(WithStyle(1),
          WithFloorMaterial("ground-tile"),
          WithCentralAirConditioning(false)))
    }
    

    功能选项模式让我们可以收获如下好处:

    • 更漂亮的、不随时间变化的公共 API;
    • 参数可读性更好;
    • 配置选项高度可扩展;
    • 提供使用默认选项的最简单方式;
    • 使用更安全(不像上一版本,创建函数被调用后仍然可以修改 options)

往期回顾

关注我

掘金:XQGang

Github: XQ-Gang

参考

《Go 语言精进之路:从新手到高手的编程思想、方法和技巧》——白明