go使用options模式设置参数

本文介绍了函数式选项模式在Go语言中的使用,如何通过该模式优雅地处理可选参数,保持接口的兼容性并方便扩展。以创建客户端为例,展示了如何定义和使用Options,以及在gRPC中的应用。此外,还提到了kratos和beego等框架对options模式的运用。

Functional Options Pattern(函数式选项模式)可用于传递不同选项配置到方法中。当每次选项参数有变更的时候,可以不改变方法或者接口的参数,这样保证了接口兼容性。使用Options的模式也更利于程序的扩展,所以在众多包中得到了广泛的使用。

下面我来具体讲解一下Options的使用方法:

1.  使用函数传递传递参数

一般我们需要创建一个服务的客户端,可能是这样的写的

type Client struct {
   url      string
   username string
   password string
}

func NewClient(url string, username string, password string,db string) *Client {
   return &Client{
      url:      url,
      username: username,
      password: password
   }
}

这么写的时候,参数都是通过New方法进行传入的。后来因为需求,又需要添加如下可选参数

readTimeout time.Duration    // 读取的超时时间
charset string               // 可以配置编码

如果按参数传递的话,我们就得修改New方法的参数了,如果有多个项目依赖这个包的话,那么引用的地方都得修改相应的New方法参数。那么怎么很好的解决这个问题,这个时候Options模型就出现了。

2. 使用options选项模式

我们首先将上述的可选参数以options方式进行定义(这里options为小写,是不对对外暴露出来具体的配置项)

type options struct { 
    readTimeout time.Duration 
    charset string 
}

然后我们定义Option的结构

type Option func(*options)

接着我们再定义配置options中参数的公开方法,用来设置参数

func WithReadTimeout(timeout time.Duration) Option {
   return func(o *options) {
      o.readTimeout = timeout
   }
}

func WithCharset(charset string) Option {
   return func(o *options) {
      o.charset = charset
   }
}

同时将options添加到Client中,并在New方法中添加相应的处理

type Client struct {
   url      string
   username string
   password string
   opts   options
}

func NewClient(url string, username string, password string,opts ...Option) *Client {
   // 创建一个默认的options
   op := options{
      readTimeout: 10,
      charset: "utf8",
   }
   // 调用动态传入的参数进行设置值
   for _, option := range opts {
      option(&op)
   }
   return &Client{
      url:      url,
      username: username,
      password: password,
      opts:    op,
   }
}

使用Options的模式后,添加参数就不需要再去修改New方法了。 添加参数我们只需要在options中添加对应的属性即可,同时配置相应的对外的WithXXX方法即可。

于是我们调用就可以使用如下方式

client := NewClient("localhost:3333","test","test",WithReadTimeout(100),WithCharset("gbk"))

后面的可以选参数可以传入任意多个Option了。这里配置的Option参数多了后,导致配置过长,我们进一步把他封装到函数中

func Options() []Option{
    return []{
        WithReadTimeout(100),
        WithCharset("gbk")
    }
}

client := NewClient("localhost:3333","test","test",Options()...)

到这里options选项模型就可以很方便的传递可选参数了,同时也不需要添加一个参数就去添加方法的参数。

3. 我们来看看其他的一些options的用户

   3.1.  gRPC中也大量使用使用options选项模式传递参数

下面是Dial方法使用options选项模式

type dialOptions struct {
   unaryInt  UnaryClientInterceptor
   streamInt StreamClientInterceptor

   chainUnaryInts  []UnaryClientInterceptor
   chainStreamInts []StreamClientInterceptor

   cp              Compressor
   dc              Decompressor
   bs              internalbackoff.Strategy
   block           bool
   returnLastError bool
   timeout         time.Duration
   scChan          <-chan ServiceConfig
   authority       string
   copts           transport.ConnectOptions
   callOptions     []CallOption
   balancerBuilder             balancer.Builder
   channelzParentID            int64
   disableServiceConfig        bool
   disableRetry                bool
   disableHealthCheck          bool
   healthCheckFunc             internal.HealthChecker
   minConnectTimeout           func() time.Duration
   defaultServiceConfig        *ServiceConfig // defaultServiceConfig is parsed from defaultServiceConfigRawJSON.
   defaultServiceConfigRawJSON *string
   resolvers                   []resolver.Builder
}

// options处理的交给实现接口类
type DialOption interface {
   apply(*dialOptions)
}

func Dial(target string, opts ...DialOption) (*ClientConn, error) {
   return DialContext(context.Background(), target, opts...)
}

---------- 具体实现apply的实现 ----------------
type funcDialOption struct {
   f func(*dialOptions)
}

func (fdo *funcDialOption) apply(do *dialOptions) {
   fdo.f(do)
}

func WithTimeout(d time.Duration) DialOption {
   return newFuncDialOption(func(o *dialOptions) {
      o.timeout = d
   })
}

上面我可以看到grpc的dial方法是另外一种实现options的方式,同时dialOptions里面还封装了其他的options,其中比较有意思是callOptions,它是一个CallOption类型数组,而CallOption是一个interface的类型,如下:

type CallOption interface {

	before(*callInfo) error

	after(*callInfo, *csAttempt)
}

这个有点像AOP一样的,为call调用之前和之后提供了前置和后置的回调。

3.2 还有其他很多框架中也大量使用options模式

   1. bilibili开源框架kratos: https://github.com/go-kratos/kratos/blob/main/options.go

   2. beego框架封装的httpclient: beego/httpclient.go at develop · beego/beego · GitHub

   3. etc...

### Golang 中为函数参数设置默认值的方法 Go 语言本身并不支持像 Python 或 JavaScript 那样的直接为函数参数指定默认值的功能。然而,通过一些设计模式和技巧,可以实现类似的默认参数功能。 #### 使用结构体作为参数并设定默认值 一种常见的方法是将所有可能的参数封装在一个结构体内,并在初始化时为其字段赋予默认值。随后,在函数调用时可以通过可变参数传递这些配置项[^1]: ```go type MyStruct struct { Param1 string Param2 int } func NewMyStruct() *MyStruct { return &MyStruct{ Param1: "default-value", Param2: 42, } } func MyFunction(config ...*MyStruct) { cfg := NewMyStruct() if len(config) > 0 && config[0] != nil { if config[0].Param1 != "" { cfg.Param1 = config[0].Param1 } if config[0].Param2 != 0 { cfg.Param2 = config[0].Param2 } } fmt.Printf("Param1=%s, Param2=%d\n", cfg.Param1, cfg.Param2) } ``` 上述代码展示了如何利用结构体来模拟默认参数的行为。 #### 利用 Functional Options 设计模式 另一种更灵活的方式是采用 Functional Options 模式。此模式的核心思想是创建一系列选项函数,用于修改目标对象的状态。这种方法不仅能够很好地处理默认值问题,还能保持接口简洁清晰[^5]: ```go type Dialer struct { Timeout time.Duration Network string } // Option 定义了一个函数类型,接受 *Dialer 并对其进行修改。 type Option func(*Dialer) // WithTimeout 是一个具体的 Option 实现,用来设置超时时间。 func WithTimeout(d time.Duration) Option { return func(dialer *Dialer) { dialer.Timeout = d } } // WithNetwork 是另一个 Option 实现,用来设置网络协议。 func WithNetwork(n string) Option { return func(dialer *Dialer) { dialer.Network = n } } // NewDialer 创建一个新的 Dialer 对象,默认值会被预先填充, // 同时允许通过 options 参数覆盖某些字段。 func NewDialer(options ...Option) *Dialer { d := &Dialer{ // 默认值 Timeout: 30 * time.Second, Network: "tcp", } for _, option := range options { option(d) } return d } ``` 这种技术使得我们可以轻松地向 `NewDialer` 添加新的行为而无需更改其签名。 #### 结合可变参数与类型断言 还可以借助可变参数列表配合类型断言机制达到类似效果。例如下面的例子演示了当未显式提供某个特定类型的参数时会应用预设值的情况[^3]: ```go import ( "fmt" ) func Concat(a interface{}, b ...interface{}) (result string) { switch v := a.(type) { case string: result += v default: panic(fmt.Sprintf("Unsupported type %T", a)) } for _, param := range b { if strVal, ok := param.(string); ok { result += strVal } else if numVal, ok := param.(int); ok { result += strconv.Itoa(numVal) } else { panic(fmt.Sprintf("Unsupported parameter type %T", param)) } } return result } ``` 在这个例子中,如果没有给出第二个整数型参数,则不会影响最终的结果计算过程。 综上所述,尽管 Go 不具备内置的支持函数参数默认值的能力,但开发者仍然有许多途径去构建具有相似特性的解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿CKeen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值