go如何实现可选参数

大家好,我是peachesTao,今天我们来聊一聊go的函数可选参数话题

go语言设计的时候函数就不支持默认参数,以下为go语言之父Rob Pike关于这个设计的一段话:

One feature missing from Go is that it does not support default function arguments. This was a deliberate simplification. Experience tells us that defaulted arguments make it too easy to patch over API design flaws by adding more arguments, resulting in too many arguments with interactions that are difficult to disentangle or even understand. The lack of default arguments requires more functions or methods to be defined, as one function cannot hold the entire interface, but that leads to a clearer API that is easier to understand. Those functions all need separate names, too, which makes it clear which combinations exist, as well as encouraging more thought about naming, a critical aspect of clarity and readability.

大概的意思是:go语言函数不支持默认参数是刻意为之,默认参数使得通过添加更多参数来修补API设计缺陷变得太容易了,导致过多的参数与交互难以解开甚至理解,为不同的可选参数定义不同函数可以使得api更清晰和理解。

假设有以下结构体:

type Conn struct {
   url      string
   timeOut  time.Duration
   failFast bool
}

现在要定义一个初始化的Conn的方法,timeOut和failFast是可选参数,如果go支持默认参数,大概是这样的:

func NewConn(url string, timeout int = 10, failFast bool = true) *Conn {
   return &Conn{
      url:      url,
      timeOut:  time.Second * time.Duration(timeout),
      failFast: failFast,
   }
}

很显然,上面的代码是编译不通过。那该如何实现呢?实现的方法有好几个,我们来一一看下

先定义默认参数值

var (
   defaultTimeOut  = time.Second * 5
   defaultFailFast = true
)

结构体作为参数

定义一个ConnOption结构体,将其作为参数传入

type ConnOption struct {
   TimeOut  time.Duration
   failFast bool
}

初始化及调用代码:

func NewConn(url string, opt ConnOption) *Conn {
   timeOut := defaultTimeOut
   if opt.TimeOut != 0 {
      timeOut = opt.TimeOut
   }
   failFast := defaultFailFast
   if !opt.failFast {
      failFast = opt.failFast
   }
   return &Conn{
      url:      url,
      timeOut:  timeOut,
      failFast: failFast,
   }
}

// 调用
NewConn("http://www.peachesTao.com", ConnOption{TimeOut: time.Second})

这种实现的弊端有两个:

  • 不传任何可选参数还必须传一个空结构体
  • NewConn方法内部不知道参数failFast的值false是用户指定的还是ConnOption初始化时默认的

我们来改进一下,将ConnOption结构中的failFast字段改成指针类型、机构体以指针的形式传入,如下:

type ConnOption struct {
   TimeOut  time.Duration
   failFast *bool
}

func NewConn(url string, opt *ConnOption) *Conn {
   timeOut := defaultTimeOut
   failFast := defaultFailFast
   if opt != nil {
      if opt.TimeOut != 0 {
         timeOut = opt.TimeOut
      }
      if opt.FailFast != nil {
         failFast = *opt.FailFast
      }
   }
   return &Conn{
      url:      url,
      timeOut:  timeOut,
      failFast: failFast,
   }
}

// 调用
NewConn("http://www.peachesTao.com", &ConnOption{TimeOut: time.Second})
failFast := false
NewConn("http://www.peachesTao.com", &ConnOption{TimeOut: time.Second, FailFast: &failFast})

failFast字段改成指针类型后NewConn就能识别用户有没有指定了,如果不想传任何可选参数可以可以传入nil

NewConn("http://www.peachesTao.com", nil)

改造后的方案虽然可以达到目的,但不够优雅:

  • 如果不想传可选参数还必须要传一个nil参数
  • 只传一个可选参数都要传一个机构体,有点重

可变参数

可以将可选参数作为可变参数以…interface{}的形式传入函数,有两种方案:

  • 固定可选参数顺序
    方法内部通过args[0],args[1]的形式获取可选参数,如果只想传后面的可选参数前面所有的可选参数都要传

  • 不固定可选参数顺序
    方法内部通过遍历args切片中的元素,根据元素类型判断属于哪个可选参数,如果有同类型的可选参数必须自定义参数类型将它们区分开来

固定可选参数顺序的方案不具备可操作性,这里不进行介绍,下面介绍第二种方案

可选参数类型都不同的情况:

func NewConn(url string, args ...interface{}) *Conn {
   timeOut := defaultTimeOut
   failFast := defaultFailFast
   for _, arg := range args {
      switch arg.(type) {
      case time.Duration:
         timeOut = arg.(time.Duration)
      case bool:
         failFast = arg.(bool)
      }
   }
   return &Conn{
      url:      url,
      timeOut:  timeOut,
      failFast: failFast,
   }
}

// 调用
NewConn("http://www.peachesTao.com", time.Second, false)

// 只传failFast
NewConn("http://www.peachesTao.com", false)

可选参数类型有重复的情况:

Conn结构体中增加一个security bool 可选参数,因其也是bool类型,跟failFast相同,所以必须自定义两种type为bool的类型来区分,代码如下:

var (
   defaultTimeOut  = time.Second * 5
   defaultFailFast = FailFast(true)
   defaultSecurity = Security(true)
)

// 自定义FailFast类型,为bool类型的别称
type FailFast bool
// 自定义Security类型,为bool类型的别称
type Security bool

type Conn struct {
   url      string
   timeOut  time.Duration
   failFast FailFast
   security Security
}
func NewConn(url string, args ...interface{}) *Conn {
   timeOut := defaultTimeOut
   failFast := defaultFailFast
   security := defaultSecurity
   for _, arg := range args { //遍历切片,根据不同的类型获取可选参数值
      switch arg.(type) {
      case time.Duration:
         timeOut = arg.(time.Duration)
      case FailFast:
         failFast = arg.(FailFast)
      case Security:
         security = arg.(Security)
      }
   }
   return &Conn{
      url:      url,
      timeOut:  timeOut,
      failFast: failFast,
      security: security,
   }
}

// 调用
NewConn("http://www.peachesTao.com", time.Second)
NewConn("http://www.peachesTao.com",FailFast(true), Security(false))
NewConn("http://www.peachesTao.com",Security(false))

这种方案唯一的不足是重复的数据类型要自定义类型

函数式选项

这其实也属于可变参数,不过实现思路不同,所以把它分开来了。
这种方案的核心思路是自定义一个func(type Option func(option *ConnOption)),func的入参是一个指向可选参数结构体的指针,然后为每个可选参数定义一个函数,入参为参数值,该函数返回前面自定义的func,并在返回函数中
将参数值赋值给可选参数结构体相应的字段:

func WithTimeOut(value time.Duration) Option {
   return func(option *ConnOption) {
      option.timeOut = value
   }
}

完整代码如下:

var (
   defaultTimeOut  = time.Second * 5
   defaultFailFast = true
   defaultSecurity = true
)

// 自定义一个func
type Option func(option *ConnOption)

// 可选参数函数,返回一个Option类型的方法
func WithTimeOut(value time.Duration) Option {
   return func(option *ConnOption) {
      option.timeOut = value
   }
}

func WithFailFast(value bool) Option {
   return func(option *ConnOption) {
      option.failFast = value
   }
}

func WithSecurity(value bool) Option {
   return func(option *ConnOption) {
      option.security = value
   }
}

// 可选参数结构体
type ConnOption struct {
   timeOut  time.Duration
   failFast bool
   security bool
}

type Conn struct {
   url      string
   timeOut  time.Duration
   failFast bool
   security bool
}

func NewConn(url string, options ...Option) *Conn {
   connOption := &ConnOption{
      timeOut:  defaultTimeOut,
      failFast: defaultFailFast,
      security: defaultSecurity,
   }
   for _, opt := range options { // 遍历传入的可选参数函数并执行
      opt(connOption)
   }
   return &Conn{
      url:      url,
      timeOut:  connOption.timeOut,
      failFast: connOption.failFast,
   }
}

//调用
NewConn("http://www.peachesTao.com", WithTimeOut(time.Second), WithFailFast(true), WithSecurity(false))
NewConn("http://www.peachesTao.com", WithFailFast(true), WithSecurity(false))
NewConn("http://www.peachesTao.com", WithSecurity(false))

这种方案巧妙的使用了返回函数,对调用者很友好,只需要调用已经定义好的可选参数函数即可,非常优雅。不过功能实现者要写比较多的代码

总结

今天介绍了三种go中实现可选参数的方案,其中可变参数和函数式选项比较优雅,具体用哪种看个人喜好了

参考

Go at Google:软件工程服务中的语言设计 https://talks.golang.org/2012/splash.article

Go 中的可选函数参数 Optional function parameters in Go · bojanz.github.io

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值