说变长参数函数被使用得最多是因为最常用的 fmt 包、log 包中的几个导出函数都是变长参数函数:
// $GOROOT/src/fmt/print.gofunc Println(a ...interface{}) (n int, err error)func Printf(format string, a ...interface{}) (n int, err error)func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)func Fprint(w io.Writer, a ...interface{}) (n int, err error)func Sprintf(format string, a ...interface{}) stringfunc Sprintln(a ...interface{}) string// $GOROOT/src/log/log.gofunc Printf(format string, v ...interface{})func Println(v ...interface{})func Fatal(v ...interface{})func Fatalf(format string, v ...interface{})func Fatalln(v ...interface{})func Panic(v ...interface{})func Panicf(format string, v ...interface{})func Panicln(v ...interface{})
1. 什么是变长参数函数
顾名思义,变长参数函数就是指函数调用时可以接受零个、一个或多个实际参数,就像下面对 fmt.Println 的调用那样:
fmt.Println() // okfmt.Println("Tony", "Bai") // okfmt.Println("Tony", "Bai", "is", "a", "gopher") // ok
对照下面 fmt.Println 函数的原型:
func Println(a ...interface{}) (n int, err error)
一个变长参数函数只能有一个 ...T 类型形式参数,并且该形式参数应该为函数参数列表中的最后一个形式参数,否则 Go 编译器就会给出如下错误提示:
func foo(args ...int, s string) int // syntax error: cannot use ... with non-final parameter argsfunc bar(args1 ...int, args2 ...string) int // syntax error: cannot use ... with non-final parameter args1
变长参数函数的”…T“类型形式参数在函数体内呈现为[]T 类型的变量,我们可以将其理解为一个 Go 语法甜头:
// variadic_function_1.gofunc sum(args ...int) int {var total int// 下面的args的类型为[]intfor _, v := range args {total += v}return total}
但在函数外部,”…T“ 类型形式参数可匹配和接受的实参类型有两种:
- 多个 T 类型变量;
- t…(t 为 []T 类型变量);
// variadic_function_1.gofunc main() {a, b, c := 1, 2, 3println(sum(a, b, c)) // 传入多个int类型的变量nums := []int{4, 5, 6}println(sum(nums...)) // 传入"nums...",num为[]int型变量}
使用变长参数函数时最容易出现的一个问题就是实参与形参的不匹配,比如下面这个例子:
// variadic_function_2.gopackage mainimport "fmt"func dump(args ...interface{}) {for _, v := range args {fmt.Println(v)}}func main() {s := []string{"Tony", "John", "Jim"}dump(s...)}
运行这段代码:
$ go run variadic_function_2.go# command-line-arguments./variadic_function_2.go:14:6: cannot use s (type []string) as type []interface {} in argument to dump
要消除编译错误,我们仅需将变量 s 的类型换为 []interface{}:
// variadic_function_2.go... ...func main() {s := []interface{}{"Tony", "John", "Jim"}dump(s...)}$ go run variadic_function_2.goTonyJohnJim
不过有个例外,那就是 Go 内置的 append 函数,它支持通过下面的方式将字符串附加到一个字节切片后面:
// variadic_function_3.gopackage mainimport "fmt"func main() {b := []byte{}b = append(b, "hello"...)fmt.Println(string(b))}$ go run variadic_function_3.gohello
2. 模拟函数重载
Go 语言不允许在同一个作用域下定义名字相同但函数原型不同的函数,如果定义这样的函数,Go 编译器会提示下面错误信息:
// variadic_function_4.gopackage mainimport ("fmt""strings")func concat(a, b int) string {return fmt.Printf("%d %d", a, b)}func concat(x, y string) string {return x + " " + y}func concat(s []string) string {return strings.Join(s, " ")}func main() {println(concat(1, 2))println(concat("hello", "gopher"))println(concat([]string{"hello", "gopher", "!"}))}$ go run variadic_function_4.go# command-line-arguments./variadic_function_4.go:9:2: too many arguments to returnhave (int, error)want (string)./variadic_function_4.go:12:6: concat redeclared in this blockprevious declaration at ./variadic_function_4.go:8:23./variadic_function_4.go:16:6: concat redeclared in this blockprevious declaration at ./variadic_function_4.go:12:26./variadic_function_4.go:21:16: too many arguments in call to concathave (number, number)want ([]string)./variadic_function_4.go:22:16: too many arguments in call to concathave (string, string)want ([]string)
如果要修复上面的例子程序,我们需要将三个”concat“函数作分别命名,比如:
concatTwoIntconcatTwoStringconcatStrings
但 Go 语言并不支持函数重载,Go 语言官方常见问答(即:FAQ)中给出的不支持的理由如下:
其他语言的经验告诉我们,使用具有相同名称但函数签名不同的多种方法有时会很有用,但在实践中也可能会造成混淆和脆弱性。 在 Go 的类型系统中,仅按名称进行匹配并要求类型一致是一个主要的简化决策。
如果要重载的函数的参数都是相同类型的,仅参数的个数是变化的,那么变长参数函数可以轻松对应;如果参数类型不同且个数可变,那么我们还要结合 interface{}类型的特性。我们来看一个例子:
// variadic_function_5.gopackage mainimport ("fmt""strings")func concat(sep string, args ...interface{}) string {var result stringfor 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))println(concat("-", "hello", "gopher"))println(concat("-", "hello", 1, uint32(2),[]int{11, 12, 13}, 17,[]string{"robot", "ai", "ml"},"hacker", 33))}
我们运行一下该例子:
$ go run variadic_function_5.go1-2hello-gopherhello-1-2-11-12-13-17-robot-ai-ml-hacker-33
3. 模拟实现函数的可选参数与默认参数
如果参数在传入时有隐式要求的固定顺序(这点由调用者保证),我们还可以利用变长参数函数模拟实现函数的可选参数和默认参数。
// variadic_function_6.gopackage mainimport "fmt"type record struct {name stringgender stringage uint16city stringcountry 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", // 默认值:Beijingcountry: "China", // 默认值:China}for i, v := range args {switch i {case 0: // namename, ok := v.(string)if !ok {return nil, fmt.Errorf("name is not passed as string")}r.name = namecase 1: // gendergender, ok := v.(string)if !ok {return nil, fmt.Errorf("gender is not passed as string")}r.gender = gendercase 2: // ageage, ok := v.(int)if !ok {return nil, fmt.Errorf("age is not passed as int")}r.age = uint16(age)case 3: // citycity, ok := v.(string)if !ok {return nil, fmt.Errorf("city is not passed as string")}r.city = citycase 4: // countrycountry, ok := v.(string)if !ok {return nil, fmt.Errorf("country is not passed as string")}r.country = countrydefault:return nil, fmt.Errorf("unknown argument passed")}}return r, nil}func main() {r, _ := enroll("小明", "male", 23)fmt.Printf("%+v\n", *r)r, _ = enroll("小红", "female", 13, "Hangzhou")fmt.Printf("%+v\n", *r)r, _ = enroll("Leo Messi", "male", 33, "Barcelona", "Spain")fmt.Printf("%+v\n", *r)r, err := enroll("小吴", 21, "Suzhou")if err != nil {fmt.Println(err)return}}
我们运行一下上面的例子:
$ go run variadic_function_6.go{name:小明 gender:male age:23 city:Beijing country:China}{name:小红 gender:female age:13 city:Hangzhou country:China}{name:Leo Messi gender:male age:33 city:Barcelona country:Spain}gender is not passed as string
如果参数特多, 并且还要让调用者记住参数顺序, 这不太可取.
我们看到基于上述前提而用 Go 实现的可选参数和默认参数是有局限的:调用者只能从右侧的参数开始逐一做省略传递的处理,比如:可以省略 country,可以省略 country、city,但不能省略 city 而不省略 country 的传递。
4. 实现”功能选项“模式
日常 Go 编程时,我们经常会去实现一些带有设置选项的创建型函数,比如:我们要创建一个网络通信的客户端,创建客户端实例的函数需要提供某种方式可以让调用者设置客户端的一些行为属性,比如:超时时间、重试次数等。对于一些复杂的 Go 包中的创建型函数,它要提供的可设置选项有时多达数十种,甚至后续还会增加。因此,设计和实现这样的创建型函数时要尤为考虑使用者的体验:不能因选项较多而提供过多的 API,并且要保证选项持续增加后,函数的对外接口依旧保持稳定。
我们来设计和实现一个 NewFinishedHouse 函数,该函数返回一个”FinishedHouse(精装房)“实例。
1) 版本 1:通过参数暴露配置选项
// variadic_function_7.gopackage mainimport "fmt"type FinishedHouse struct {style int // 0: Chinese, 1: American, 2: EuropeancentralAirConditioning bool // true or falsefloorMaterial 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 passedh := &FinishedHouse{style: style,centralAirConditioning: centralAirConditioning,floorMaterial: floorMaterial,wallMaterial: wallMaterial,}return h}func main() {fmt.Printf("%+v\n", NewFinishedHouse(0, true, "wood", "paper"))}
运行该例子:
$ go run variadic_function_7.go&{style:0 centralAirConditioning:true floorMaterial:wood wallMaterial:paper}
上述这个设计的唯一优点就是能够快速实现,但不足之处却有很多,最致命的是该接口没法扩展。如果我们此时应用户要求增加一个室内门型设置的选项(可选实木门/板材套装门),那么该接口无法满足。考虑兼容性原则,该接口一但发布就成为了 API 的一部分,我们不能随意变更。于是我们唯一能做的就是新增一个创建函数,比如:NewFinishedHouseWithDoorOption。如果后续要增加其他设置选项,API 中很大可能会充斥着 NewFinishedHouseWithXxxOption1、NewFinishedHouseWithYyyOpiton、… NewFinishedHouseWithZzzOption 等新接口。
2) 版本 2:使用结构体封装配置选项
// variadic_function_8.gopackage mainimport "fmt"type FinishedHouse struct {style int // 0: Chinese, 1: American, 2: EuropeancentralAirConditioning bool // true or falsefloorMaterial string // "ground-tile" or ”wood"wallMaterial string // "latex" or "paper" or "diatom-mud"}type Options struct {Style int // 0: Chinese, 1: American, 2: EuropeanCentralAirConditioning bool // true or falseFloorMaterial 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 nilvar style int = 0var centralAirConditioning = truevar floorMaterial = "wood"var wallMaterial = "paper"if options != nil {// here: you should do some check to the options passedstyle = options.StylecentralAirConditioning = options.CentralAirConditioningfloorMaterial = options.FloorMaterialwallMaterial = options.WallMaterial}h := &FinishedHouse{style: style,centralAirConditioning: centralAirConditioning,floorMaterial: floorMaterial,wallMaterial: wallMaterial,}return h}func main() {fmt.Printf("%+v\n", NewFinishedHouse(nil)) // use default optionsfmt.Printf("%+v\n", NewFinishedHouse(&Options{Style: 1,CentralAirConditioning: false,FloorMaterial: "ground-tile",WallMaterial: "paper",}))}
我们运行一下这个例子:
$ go run variadic_function_8.go&{style:0 centralAirConditioning:true floorMaterial:wood wallMaterial:paper}&{style:1 centralAirConditioning:false floorMaterial:ground-tile wallMaterial:paper}
我们看到:
- 使用这种方法,即便后续添加新配置选项,Options 结构体可以随着时间变迁而增长,但 FinishedHouse 创建函数本身的 API 签名是保持不变的;
- 这种方法还使得调用者可以使用 nil 来表示他们希望使用默认配置选项来创建 FinishedHouse;
- 这种方法还带来了额外收获:更好的文档记录(文档重点从对 NewFinishedHouse 函数的大段注释描述转移到了对 Options 结构体各字段的说明)。
当然这种方法也有其不足的地方:
- 调用者可能会有如此疑问:传递 nil 和传递&Options{}之间有区别吗?
- 每次传递 Options 都要将 Options 中的所有字段做正确显式的赋值,即便调用者想使用某个配置项的默认值,赋值动作 1 依然不可少;
- 调用者还可能有如此疑问:如果传递给 NewFinishedHourse 的 options 中的字段值在函数调用后发生了变化会发生什么情况?
3) 版本 3:使用“功能选项”模式
// variadic_function_9.gopackage mainimport "fmt"type FinishedHouse struct {style int // 0: Chinese, 1: American, 2: EuropeancentralAirConditioning bool // true or falsefloorMaterial 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 optionsstyle: 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 optionsfmt.Printf("%+v\n", NewFinishedHouse(WithStyle(1),WithFloorMaterial("ground-tile"),WithCentralAirConditioning(false)))}
运行一下该新版例子:
$ go run variadic_function_9.go&{style:0 centralAirConditioning:true floorMaterial:wood wallMaterial:paper}&{style:1 centralAirConditioning:false floorMaterial:ground-tile wallMaterial:paper}
功能选项模式使得我们在设计和实现类似 NewFinishedHouse 这样带有配置选项的函数或方法时可以收获如下好处:
- 更漂亮的、不随时间变化的公共 API
- 参数可读性更好
- 配置选项高度可扩展 (不依赖顺序)
- 提供使用默认选项的最简单方式
- 使用更安全(不会像版本 2 那样在创建函数被调用后,调用者仍然可以修改 options)
