在日常Go编程中,我们经常会实现一些带有设置选项的创建型函数。
比如:我们要创建一个网络通信的客户端,创建客户端实例的函数需要提供某种方式以让调用者设置客户端的一些行为属性,如超时时间、重试次数等。对于一些复杂的Go包中的创建型函数,它要提供的可设置选项有时多达数十种,甚至后续还会增加。因此,设计和实现这样的创建型函数时要尤为注意、考虑使用者的体验,不能因选项较多而提供过多的API,并且要保证选项持续增加后,函数的对外接口依旧保持稳定。
接下来就让我们通过一个简单的示例来看看变长参数函数在这里究竟能发挥什么作用。我们先从一个简单的版本开始并对其进行持续优化,直到实现令我们满意的最终版本。我们来设计和实现一个NewFinishedHouse函数,该函数返回一个FinishedHouse(精装房)实例。精装房是有不同装修选项的,比如以下常见选项。
装修风格:美式、中式或欧式。是否安装中央空调系统。地面材料:瓷砖或实木地板。墙面材料:乳胶漆、壁纸或硅藻泥。可能还有很多装修配置选项,但这里使用这几个就足以满足示例的需要了。
版本1:通过参数暴露配置选项
一个最简单、直接的实现方法就是通过函数参数暴露配置选项,让调用者自行设置自己所需要的精装房风格和使用的材料
--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
go
type FinishedHouse struct {
style int // 0: Chinese; 1: American; 2: European
centralAirConditioning bool // true或false
floorMaterial string // "ground-tile"或"wood"
wallMaterial string // "latex" "paper"或"diatom-mud"
}
func NewFinishedHouse(style int, centralAirConditioning bool,
floorMaterial, wallMaterial string) *FinishedHouse {
h := &FinishedHouse{
style: style,
centralAirConditioning: centralAirConditioning,
floorMaterial: floorMaterial,
wallMaterial: wallMaterial,
}
return h
}
func main() {
fmt.Printf("%+v\n", NewFinishedHouse(0, true, "wood", "paper"))
}
运行该例子:
--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
go
go run variadic_function_7.go
&{style:0 centralAirConditioning:true floorMaterial:wood wallMaterial:paper}
版本2:使用结构体封装配置选项
软件设计中的一个比较重要的原则是封装变化。既然我们无法控制将来要加入的配置选项的个数和内容,但还要尽可能保持提供单一接口,那我们就把配置选项这个变量抽取出来并封装到一个结构体中,这也是目前比较常见的做法。
下面是我们的第二个版本:
--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
go
type FinishedHouse struct {
style int // 0: Chinese; 1: American; 2: European
centralAirConditioning bool // true或false
floorMaterial string // "ground-tile"或"wood"
wallMaterial string // "latex" "paper"或"diatom-mud"
}
type Options struct {
Style int // 0: Chinese; 1: American; 2: European
CentralAirConditioning bool // true或false
FloorMaterial string // "ground-tile"或"wood"
WallMaterial string // "latex" "paper"或"diatom-mud"
}
func NewFinishedHouse(options *Options) *FinishedHouse {
// 如果options为nil,则使用默认的风格和材料
var style int = 0
var centralAirConditioning = true
var floorMaterial = "wood"
var wallMaterial = "paper"
if options != nil {
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)) // 使用默认值
fmt.Printf("%+v\n", NewFinishedHouse(&Options{
Style: 1,
CentralAirConditioning: false,
FloorMaterial: "ground-tile",
WallMaterial: "paper",
}))
}
运行一下这个例子:
--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
go
$ 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来表示他们希望使用默认配置选项来创建Finished-House;
这种方法还带来了额外收获------更好的文档记录(文档重点从对NewFinishedHouse函数的大段注释描述转移到了对Options结构体各字段的说明)。
当然这种方法也有不足的地方:
调用者可能会有如此疑问,传递nil和传递&Options{}之间有区别吗?
每次传递Options都要为Options中的所有字段进行显式赋值,即便调用者想使用某个配置项的默认值,赋值动作依然不可少;
调用者还可能有如此疑问,如果传递给NewFinishedHourse的options中的字段值在函数调用后发生了变化,会出现什么情况?
带着这些疑问,我们进入NewFinishedHouse的下一个版本。
版本3:使用函数选项模式
这种模式应该是目前进行功能选项设计的最佳实践。接下来我们就来看看使用功能选项模式实现的NewFinishedHouse是什么样的:
--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
go
type FinishedHouse struct {
style int // 0: Chinese; 1: American; 2: European
centralAirConditioning bool // true或false
floorMaterial string // "ground-tile"或"wood"
wallMaterial string // "latex"或"paper"或"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()) // 使用默认选项
fmt.Printf("%+v\n", NewFinishedHouse(WithStyle(1),
WithFloorMaterial("ground-tile"),
WithCentralAirConditioning(false)))
}
运行一下该新版例子:
--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
go
$ go run variadic_function_9.go
&{style:0 centralAirConditioning:true floorMaterial:wood wallMaterial:paper}
&{style:1 centralAirConditioning:false floorMaterial:ground-tile wallMaterial:paper}
我们看到在该方案中,FinishedHouse的配置选项不是通过存储在结构体中的配置参数传入的,而是通过对FinishedHouse值本身进行操作的函数调用(利用函数的"一等公民"特质)实现的,并且通过使用变长参数函数,我们可以随意扩展传入的配置选项的个数。
在设计和实现类似NewFinishedHouse这样带有配置选项的函数或方法时,功能选项模式让我们可以收获如下好处:
更漂亮的、不随时间变化的公共API;
参数可读性更好;配置选项高度可扩展;
提供使用默认选项的最简单方式;
使用更安全(不会像版本2那样在创建函数被调用后,调用者仍然可以修改options)。
最后
在这一条中我们了解了我们日常使用最多却经常忽视的一类函数------变长参数函数,学习了它的原理以及如何通过它在特定场合简化代码逻辑。
本条要点:了解变长参数函数的特点和约束;
变长参数函数可以在有限情况下模拟函数重载、可选参数和默认参数,但要谨慎使用,不要造成混淆;
利用变长参数函数实现功能选项模式。