在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
}
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中实现的方法。

看似很简单的一个功能,好的开源代码总会采用各种方式,值得我们在实际项目中多多体会,如果大家有什么更好的方法欢迎留言交流分享。


laughbug
275 声望2 粉丝

Quitters never win and winners never quit.