在 go 的我的项目中,大家编码的时候应该或多或少都看过 go 的一些源码或者其余开源我的项目的源码,不晓得大家有没有感觉本人写进去的代码绝对于源码有肯定的差距,不论从构造定义,接口封装等方面总感觉差那么点意思。反正我始终感觉本人的编码绝对于 go 的源码差距较大。既然有这么好的代码在背后,咱们如何依据这些源码提取一些供本人学习的内容呢,这次筹备依据源码好好学习一下,简略总结如下,如果后续还有心得会逐渐更新。
最近正好用到的两个开源代码:
1、github.com/olivere/elastic/v7
2、google.golang.org/grpc
本人的我的项目调用这两个包时,有如下的代码(如果有趣味能够本人去 github 上看残缺的源码)
1、用 elastic 包构建聚合查问
agg := elastic.NewTermsAggregation().
Size(10000).
Field("data.sce")
2、用 grpc 结构连贯
conn, err := grpc.DialContext(connCtx, "127.0.0.1:5555", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {return err}
抛开两个函数的性能不谈,其实两个函数都能够了解为一种结构或者初始化函数,其 Size,Field,WithInsecure,WithBlock 等函数都是在初始化或者结构时提供某种参数而已。
那咱们提炼一下,就是当咱们结构一个对象时,个别会提供很多参数用来结构,然而不同场景,或者不同条件下,须要的参数又不同,如何来封装这些构造函数来方便使用呢?
以如下构造为例,假如是一个代理
type Agency struct {
IP int32 //required
Protocol string //optional
Timeout time.Duration //optional
}
办法一:
间接结构不同的构造函数:
func NewAgency(ip int32) *Agency
func NewAgencyWithProtocol(ip int32, p string) *Agency
func NewAgencyWithProtocolAndTimeout(ip int32, p string, t time.Duration) *Agency
......
你会发现须要定义一系列的函数,而且随着参数增多,可能裁减的函数也比拟多,同时因为 go 中不反对多态,每个函数还要有不同的名称,当你这次调用了 A,下次减少参数时,还得改为调用 B,可见麻烦多多,既不难看,也不好用,还不好保护。当然理论我的项目中可能也没人会这么写,这里只是举例而已。
办法二:
间接将 Agency 构造体作为参数,这样参数不就固定了吗,而且一举解决所有问题,只用一个构造函数即可。
func NewAgency(a Agency) *Agency
这种办法事实我的项目中的确也有应用的哦,那对于这种简略的构造体,还算比拟不便简洁,然而如果构造体成员较多(像 goroutine 等构造),动辄 20+ 以上,那你初始化的时候还得先一一确定参数,而后再去调用构造函数是不是也麻烦,而且很多参数其实用不到赋值,只有默认值就够用了。而且成员变量一多,你可能都不晓得那些是必选参数,那些是可选参数了。
那如何解决这个问题呢?
办法三:
批改 Agency 的构造体,将可选成员和必选成员离开:
type AgencyOption struct {
Protocol string //optional
Timeout time.Duration //optional
TLS tls.Conn //optional
}
type Agency struct {
IP int32 //required
AgencyOption
}
而后定义一个构造函数
func NewAgency(ip int32, param *AgencyOption) *Agency
这个函数辨别了必选项和可选项,ip 必填,param 可选,如果 param 为 nil 则不必对可选参数赋值。
这种计划绝对于上一个办法略有改良,对于必选参数高深莫测,然而对于参数较多的场景还是没有基本解决。
办法四:
间接将可选参数不放在构造函数中,定义多个设置函数,例如:
func (a *Agency)SetProtocol(p string) {a.Protocol = p}
func (a *Agency)SetTimeout(t time.Duration) {a.Timeout = t}
调用者应用形式:
a1 := NewAgency(ip)
a1.SetProtocol("udp")
a1.SetTimeout(100)
办法五:
上述办法须要屡次调用,咱们做个批改:
func (a *Agency)SetProtocol(p string) *Agency{
a.Protocol = p
return a
}
func (a *Agency)SetTimeout(t time.Duration) *Agency{
a.Timeout = t
return a
}
这样,调用者能够应用链式调用:
a1 := NewAgency(ip).SetTimeout(100).SetProtocol("udp")
这也是一种罕用的办法。
像文章开篇提到的 elastic 库就是这么玩的,每次查问的时候可能有很多参数要设置,间接间断调用即可,很清晰,这也是从这个开源库中学到。当前就能够间接利用到理论我的项目中了哦!
那如果就想将参数一把传入,一次性初始化实现呢?
办法六:
一次性传入任意多个参数,首先咱们能够想到 go 反对变参,例如 func Add(base int, others …int),能够解决任意个数的 int 类型,然而咱们的参数个别是不一样的,那咱们如何利用这种计划呢?
咱们大胆设想一下如果有这么一种通用类型能够应用(先不思考返回值):
func NewAgency(ip int32, options ...Option)
如果能实现这样一个构造函数,那么可选参数的问题也就搞定了!!
然而这个通用的 Option 如何定义呢??应用某一种具体类型必定是不能实现的,那是否能够将这个 Option 定义为一个函数或者接口类型呢?
先从函数思考,看是否实现:
一个函数,无非是函数名,入参,逻辑解决,返回值这些东东
函数名,whatever,轻易起个能自正文的即可;
入参,先放一下;
逻辑解决,就是这个函数要做啥,想想咱们的最终目标就是将可选参数设置到咱们的对象中去!!那么这个逻辑解决就相似于:
agency.Timeout = time
以及
agency.Protocol = "udp"
等等...
从逻辑解决看咱们要将参数设置到对象中,那这个对象是不是能够作为咱们的独特参数,那么这个 Option 的定义是不是能够为:
type Option func(a *Agency)
因为是要扭转 Agency 中的值,所以用的是指针作为入参。
既然 Option 定义好了,那么针对每个参数咱们来实现由参数如何转换为这种 Option 传入构造函数吧,即创立入参是参数,然而返回值是 Option 类型的函数
对于超时工夫:
func Timeout(t time.Duration) Option{return func(a *Agency){a.Timeout = t}
}
对于协定设置:
func Protocol(p string) Option{return func(a *Agency){a.Protocol = p}
}
构造函数为:
func NewAgency(ip int32, options ...Option) *Agency {a := &Agency{IP:ip}
for _, opt := range options{opt(a)
}
return a
}
调用者为:
a1 := NewAgency(ip)
a2 := NewAgency(ip, Timeout(100))
a4 := NewAgency(ip, Timeout(200), Protocol("udp"))
这样就清晰明了了吧,功败垂成!
办法七:
上个办法中定义的 Option 是一个函数类型,那么接口类型是否也能够胜任呢?
答案也是能够的,这个是我从 grpc 的实现反向思考的过程,大家能够参考,或者间接撸 grpc 的源码看:)
咱们还是以 Agency 为例
首先定义这个 Option 接口,因为在 go 中通常定义的接口名都带 er,咱们也遵循传统定义为:
type Optioner interface {apply()
}
接口先只定义了一个名字,入参和返回值待定。
既然有了接口,那么咱们就要定义一个类型来实现这个接口:
type RealOption struct {
}
func (ro *RealOption)apply(){}
雏形就有了,那么如何来填这些定义的内容呢?
先别急,咱们持续定义设置参数的函数,返回值类型都要为 Optioner,所以其模型相似为:
func SetProtocol(p string) Optioner {return &RealOption{}
}
func SetTimeout(t time.Duration) Optioner {return &RealOption{}
}
再持续看咱们最终可能提供的构造函数,应该是这个样子:
func NewAgency(ip int32, options ...Optioner) *Agency {a := &Agency{IP:ip}
for _, opt := range options{opt.apply()
}
return a
}
外围还是遍历可变参数 options,去调用对应的接口设置相应的参数,从这里作为突破口,那么 apply 这个接口类型应该定义成什么呢,是不是跃然纸上了,只有减少个入参,无需返回值
apply(agency *Agency)
有了入参后,上述波及参数的各个定义批改为:
func (ro *RealOption)apply(agency *Agency){
}
type Optioner interface {apply(agency *Agency)
}
func NewAgency(ip int32, options ...Optioner) *Agency {a := &Agency{IP:ip}
for _, opt := range options{opt.apply(a)
}
return a
}
既然接口定义好了,那么看如何实现参数设置函数的逻辑,对于 SetTimeout 函数,目标是将入参 t 传到对象中去,那么就相似于:
func SetTimeout(t time.Duration) Optioner {
return &RealOption{?:t,}
}
如果? 的地位是具体的类型,那么这个 t 其实是设置到了 RealOption 中,并没有设置到 Agency 中,那么怎么办呢,还记得上个办法中的 Option 定义吗,其就是一个通用类型的函数,将参数设置到 Agency 里,那么这个中央也用这种形式呢,即:
func SetTimeout(t time.Duration) Optioner {
return &RealOption{?:func(agency *Agency){agency.Timeout = t},
}
}
那这样 RealOption 的定义也就进去了:
type RealOption struct {f func(agency *Agency)
}
那么所有的内容根本都实现了,整体代码如下:
type Optioner interface {apply(agency *Agency)
}
type RealOption struct {f func(agency *Agency)
}
func (ro *RealOption) apply(agency *Agency) {ro.f(agency)
}
func SetProtocol(p string) Optioner {
return &RealOption{f: func(agency *Agency) {agency.Protocol = p},
}
}
func SetTimeout(t time.Duration) Optioner {
return &RealOption{f: func(agency *Agency) {agency.Timeout = t},
}
}
func NewAgency(ip int32, options ...Optioner) *Agency {a := &Agency{IP: ip}
for _, opt := range options {opt.apply(a)
}
return a
}
调用形式跟上一种办法一样,不过传入的可选参数是接口而已
持续优化一下,对 RealOption 构造也提供一个构造函数,那么参数设置函数改为:
func NewRealOption(f func(agency *Agency)) *RealOption {
return &RealOption{f: f,}
}
func SetProtocol(p string) Optioner {return NewRealOption(func(agency *Agency) {agency.Protocol = p})
}
func SetTimeout(t time.Duration) Optioner {return NewRealOption(func(agency *Agency) {agency.Timeout = t})
}
OK, 功败垂成!这种办法就对应了开篇提到的 grpc 中实现的办法。
看似很简略的一个性能,好的开源代码总会采纳各种形式,值得咱们在理论我的项目中多多领会,如果大家有什么更好的办法欢送留言交换分享。