Golang技巧之默认值的设置
最近使⽤ GRPC 发现⼀个设计特别好的地⽅,⾮常值得借鉴。
我们在⽇常写⽅法的时候,希望给某个字段设置⼀个默认值,不需要定制化的场景就不传这个参数,但是 Golang 却没有提供像
PHP、Python 这种动态语⾔设置⽅法参数默认值的能⼒。
低阶玩家应对默认值问题
以⼀个购物车举例。⽐如我有下⾯这样⼀个购物车的结构体,其中 CartExts 是扩展属性,它有⾃⼰的默认值,使⽤者希望如果不改变默认值时就不传该参数。但是由于 Golang ⽆法在参数中设置默认值,只有以下⼏个选择:
1. 提供⼀个初始化函数,所有的 ext 字段都做为参数,如果不需要的时候传该类型的零值,这把复杂度暴露给调⽤者;
2. 将 ext 这个结构体做为⼀个参数在初始化函数中,与 1 ⼀样,复杂度在于调⽤者;
3. 提供多个初始化函数,针对每个场景都进⾏内部默认值设置。
下⾯看下代码具体会怎么做
const (
CommonCart = "common"
BuyNowCart = "buyNow"
)
type CartExts struct {
CartType string
TTL      time.Duration
}
type DemoCart struct {
UserID string
ItemID string
Sku    int64
Ext    CartExts
}
var DefaultExt = CartExts{
CartType: CommonCart,      // 默认是普通购物车类型
TTL:      time.Minute * 60, // 默认 60min 过期
}
// ⽅式⼀:每个扩展数据都做为参数
func NewCart(userID string, Sku int64, TTL time.Duration, cartType string) *DemoCart {
ext := DefaultExt
if TTL > 0 {
ext.TTL = TTL
}
if cartType == BuyNowCart {
ext.CartType = cartType
}
return &DemoCart{
UserID: userID,
Sku:    Sku,
Ext:    ext,
}
}
// ⽅式⼆:多个场景的独⽴初始化函数;⽅式⼆会依赖⼀个基础的函数
func NewCartScenes01(userID string, Sku int64, cartType string) *DemoCart {
return NewCart(userID, Sku, time.Minute*60, cartType)
}
func NewCartScenes02(userID string, Sku int64, TTL time.Duration) *DemoCart {
return NewCart(userID, Sku, TTL, "")
}
上⾯的代码看起来没什么问题,但是我们设计代码最重要的考虑就是稳定与变化,我们需要做到 对扩展开放,对修改关闭 以及代码的 ⾼内聚。那么如果是上⾯的代码,你在 CartExts 增加了⼀个字段或者减少了⼀个字段。是不是每个地⽅都需要进⾏修改呢?⼜或者 CartExts 如果有⾮常多的字段,这个不同场景的构造函数是不是得写⾮常多个?所以简要概述⼀下上⾯的办法存在的问题。
1. 不⽅便对 CartExts 字段进⾏扩展;
2. 如果 CartExts 字段⾮常多,构造函数参数很长,难看、难维护;
3. 所有的字段构造逻辑冗余在 NewCart 中,⾯条代码不优雅;
4. 如果采⽤ CartExts 做为参数的⽅式,那么就将过多的细节暴露给了调⽤者。
接下来我们来看看 GRPC 是怎么做的,学习优秀的范例,提升⾃我的代码能⼒。
从这你也可以体会到代码功底⽜逼的⼈,代码就是写的美!
GRPC 之⾼阶玩家设置默认值
源码来⾃:grpc@v1.28.1 版本。为了突出主要⽬标,对代码进⾏了必要的删减。
// dialOptions 详细定义在 /
type dialOptions struct {
// ... ...
insecure    bool
timeout    time.Duration
// ... ...
}
// ClientConn 详细定义在 /
type ClientConn struct {
// ... ...
authority    string
dopts        dialOptions // 这是我们关注的重点,所有可选项字段都在这⾥
csMgr        *connectivityStateManager
// ... ...
}
// 创建⼀个 grpc 链接
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
cc := &ClientConn{
target:            target,
csMgr:            &connectivityStateManager{},
conns:            make(map[*addrConn]struct{}),
dopts:            defaultDialOptions(), // 默认值选项
blockingpicker:    newPickerWrapper(),
czData:            new(channelzData),
firstResolveEvent: grpcsync.NewEvent(),
}
// ... ...
// 修改改选为⽤户的默认值
for _, opt := range opts {
opt.apply(&cc.dopts)
}
// ... ...
}
上⾯代码的含义⾮常明确,可以认为 DialContext 函数是⼀个 grpc 链接的创建函数,它内部主要是构建 ClientConn 这个结构体,并做为返回值。defaultDialOptions 函数返回的是系统提供给 dopts 字段的默认值,如果⽤户想要⾃定义可选属性,可以通过可变参数 opts 来控制。
经过上⾯的改进,我们惊奇的发现,这个构造函数⾮常的优美,⽆论 dopts 字段如何增减,构造函数
不需要改动;defaultDialOptions 也可以从⼀个公有字段变为⼀个私有字段,更加对内聚,对调⽤者友好。
那么这⼀切是怎么实现的?下⾯我们⼀起学习这个实现思路。
DialOption 的封装
⾸先,这⾥的第⼀个技术点是,DialOption 这个参数类型。我们通过可选参数⽅式优化了可选项字段修改时就要增加构造函数参数的尴尬,但是要做到这⼀点就需要确保可选字段的类型⼀致,实际⼯作中这是不可能的。所以⼜使出了程序界最⾼⼿段,⼀层实现不了,就加⼀层。
通过这个接⼝类型,实现了对各个不同字段类型的统⼀,让构造函数⼊参简化。来看⼀下这个接⼝。
type DialOption interface {
apply(*dialOptions)
}
这个接⼝有⼀个⽅法,其参数是 *dialOptions 类型,我们通过上⾯ for 循环处的代码也可以看到,传⼊的是 &cc.dopts。简单说就是把要修改的对象传⼊进来。apply ⽅法内部实现了具体的修改逻辑。
那么,这既然是⼀个接⼝,必然有具体的实现。来看⼀下实现。
// 空实现,什么也不做
type EmptyDialOption struct{}
func (EmptyDialOption) apply(*dialOptions) {}
// ⽤到最多的地⽅,重点讲
type funcDialOption struct {
f func(*dialOptions)
}
func (fdo *funcDialOption) apply(do *dialOptions) {
fdo.f(do)
}
func newFuncDialOption(f func(*dialOptions)) *funcDialOption {
return &funcDialOption{
f: f,
}
}
我们重点说 funcDialOption 这个实现。这算是⼀个⾼级⽤法,体现了在 Golang ⾥边函数是 ⼀等公民。它有⼀个构造函数,以及实现了DialOption 接⼝。
newFuncDialOption 构造函数接收⼀个函数做为唯⼀参数,然后把传⼊的函数保存到 funcDialOption 的字段 f 上。再来看看这个参数函数的参数类型是 *dialOptions ,与 apply ⽅法的参数是⼀致的,这是设计的第⼆个重点。
现在该看 apply ⽅法的实现了。它⾮常简单,其实就是调⽤构造 funcDialOption 时传⼊的⽅法。可以理解为相当于做了⼀个代理。把 apply 要修改的对象丢到 f 这个⽅法中。所以重要的逻辑都是我们传⼊到 newFuncDialOption 这个函数的参数⽅法实现的。
现在来看看 grpc 内部有哪些地⽅调⽤了 newFuncDialOption 这个构造⽅法。
newFuncDialOption 的调⽤
由于 newFuncDialOption 返回的 *funcDialOption 实现了 DialOption 接⼝,因此关注哪些地⽅调⽤了它,就可以顺藤摸⽠的到我们最初grpc.DialContext 构造函数 opts 可以传⼊的参数。
调⽤了该⽅法的地⽅⾮常多,我们只关注⽂章中列出的两个字段对应的⽅法:insecure 与 timeout。
// 以下⽅法详细定义在 /
// 开启不安全传输
func WithInsecure() DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.insecure = true
})
}
// 设置 timeout
func WithTimeout(d time.Duration) DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.timeout = d
})
}
来体验⼀下这⾥的精妙设计:
1. ⾸先对于每⼀个字段,提供⼀个⽅法来设置其对应的值。由于每个⽅法返回的类型都是 DialOption ,从⽽确保了 grpc.DialContext ⽅
法可⽤可选参数,因为类型都是⼀致的;
2. 返回的真实类型是 *funcDialOption ,但是它实现了接⼝ DialOption,这增加了扩展性。
grpc.DialContext 的调⽤
完成了上⾯的程序构建,现在我们来站在使⽤的⾓度,感受⼀下这⽆限的风情。
opts := []grpc.DialOption{
grpc.WithTimeout(1000),
grpc.WithInsecure(),
}
conn, err := grpc.DialContext(context.Background(), target, )
python新手代码userid// ... ...
当然这⾥要介绍的重点就是 opts 这个 slice ,它的元素就是实现了 DialOption 接⼝的对象。⽽上⾯的两个⽅法经过包装后都是
*funcDialOption 对象,它实现了 DialOption 接⼝,因此这些函数调⽤后的返回值就是这个 slice 的元素。
现在我们可以进⼊到 grpc.DialContext 这个⽅法内部,看到它内部是如何调⽤的。遍历 opts,然后依
次调⽤ apply ⽅法完成设置。
// 修改改选为⽤户的默认值
for _, opt := range opts {
opt.apply(&cc.dopts)
}
经过这样⼀层层的包装,虽然增加了不少代码量,但是明显能够感受到整个代码的美感、可扩展性都得到了改善。接下来看⼀下,我们⾃⼰的 demo 要如何来改善呢?
改善 DEMO 代码
⾸先我们需要对结构体进⾏改造,将 CartExts 变成 cartExts, 并且需要设计⼀个封装类型来包裹所有的扩展字段,并将这个封装类型做为构造函数的可选参数。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。