在软件开发领域,Options 模式(Options Pattern)是一种常见的设计模式,它允许用户通过提供一系列选项来自定义函数、类型或对象的行为。在 Golang 中,选项模式的应用非常广泛,尤其是在库和框架的设计中。接下来将深入探讨 Golang 中选项模式的实现方式,以及如何利用选项模式提高代码的灵活性和可维护性。
重新认识 type 关键字
type 是 go 语法里的重要而且常用的关键字。搞清楚 type 的使用,就容易理解 go 语言中的核心概念 struct、interface、函数等的使用。
类型定义
使用 type 可以定义结构体、接口、函数,以及自定义类型:
type Preson struct { name string age int}type USB interface { start() end()}type function func() inttype Duration int64可以看到都是通过语法:type "自定义类型名称" "基于已有类型或者结构定义"进行类型的定义。那么以type Duration int64为例,Duration 和 int64 之间是什么关系呢?
其实 Duration 类型相比于 int64 类型, Duration 除了能表示 int64 类型所表示的数值,还具有了一定的语义再里面。
-
一个比较贴切的例子来说明 Duration 和 int64 之间的关系,可以用度量单位和数字之间的关系来比喻。
-
假如你有一个数字 60。这个数字本身没有特定的含义,它只是一个数值。它可以代表分钟、秒钟、天数,或者仅仅是一个数量。这就像 int64 类型,它是一个基本的数据类型,可以用来存储任何数值,但它本身不提供任何关于这个数值用途的上下文信息。
-
现在,如果我们说这个 60 是分钟,那么它突然有了具体的时间含义。这就类似于把 int64 转换为 Duration 类型。通过明确这是 60 分钟,我们用一个具体的单位(分钟)赋予了这个数字一个特定的上下文和用途。Duration 作为一种类型,它带有时间的语义,告诉我们这不仅仅是一个数字,而是一个时间间隔。
通过 type 定义新类型的这种做法通常用于增强类型安全,使代码更清晰、易于维护,并且可以附加特定于该类型的方法。这样的新类型并不会继承原始类型的方法,也不能隐式转换。
当声明 type Duration int64 时,Duration 成为了 int64 的一个新的类型,但它们在 Go 语言的类型系统中是不兼容的。尽管 Duration 底层使用的是 int64 的存储方式,但你不能直接将一个 int64 类型的变量赋值给一个 Duration 类型的变量,反之亦然。它们之间的赋值都需要显式类型转换。
type Duration int64var time Durationvar num int64time = Duration(num)num = int64(time)类型别名
type TypeAlias = Type类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。也就是两种类型完全等价,没有任何区别。下面请看 Demo:
// Address 结构体表示一个地址type Address struct { Street string PostalCode int}type HomeAddress = Addresstype WorkAddress = Address// PrintHomeAddress 打印家庭地址的方法func (h *HomeAddress) PrintHomeAddress() { fmt.Printf("%s, %d\n", h.Street, h.PostalCode)}// PrintWorkAddress 打印工作地址的方法func (w *WorkAddress) PrintWorkAddress() { fmt.Printf("%s, %d\n", w.Street, w.PostalCode)}func main() { home := HomeAddress{ Street: "123 Maple St", PostalCode: 90210, } work := WorkAddress{ Street: "456 Oak St", PostalCode: 10001, } // 两个类型完全等价,可以互相调用对应定义的方法 home.PrintWorkAddress() work.PrintHomeAddress()}函数选项
Demo 示例
函数选项是一种通过函数参数来传递选项信息的方式。这种方式可以使代码更加清晰和易于扩展,同时提供了更灵活的定制能力。下面是一个简单的连接 redis 的 Demo 代码,使用函数选项来设置一些非必要的连接参数。
import "fmt"// ClientOptions 中的network,address为必填type ClientOptions struct { maxIdleConns int timeout int wait bool}type ClientOption func(c *ClientOptions)// WithMaxIdleConns 设置最大空闲连接数func WithMaxIdleConns(maxIdleConns int) ClientOption { return func(c *ClientOptions) { c.maxIdleConns = maxIdleConns }}// WithTimeout 设置连接池释放连接的超时连接func WithTimeout(timeout int) ClientOption { return func(c *ClientOptions) { c.timeout = timeout }}// WithWaitMode 设置是否阻塞func WithWaitMode(wait bool) ClientOption { return func(c *ClientOptions) { c.wait = wait }}// Client 表示 Redis 客户端type Client struct { network string address string options *ClientOptions}// NewClient 新建客户端, 适用于简单或标准的Redis连接需求func NewClient(network, address string, opts ...ClientOption) *Client { if network == "" || address == "" { return nil } options := &ClientOptions{} for _, opt := range opts { opt(options) } return &Client{ network: network, address: address, options: options, }}func main() { client := NewClient("tcp", "localhost:6379", WithMaxIdleConns(10), WithTimeout(300), WithWaitMode(true)) if client == nil { panic("client is nil") } fmt.Println(client.options.maxIdleConns) fmt.Println(client.options.timeout) fmt.Println(client.options.wait)}运行结果如下:
PS D:\GoProjects\main> go run main.go10300true主要是 NewClient 函数内部:
- 该函数的
opts参数是一个[]ClientOption类型的切片,而 ClientOption 类型则是一个func(c *ClientOptions)的函数类型。 - 通过遍历 opts 切片得到 opt,那么 opt 就是
func(c *ClientOptions)函数。在调用 WithWaitMode(true)的时候,其实就是调用函数func(c *ClientOptions),该函数会对options指针指向的ClientOptions结构体实例进行操作,从而修改某一个字段。
优点:
- 灵活性和可扩展性:函数选项模式允许开发者以增量的方式添加或修改配置,而不需要更改已有的函数签名或调用方式。这使得在不破坏现有代码的前提下增加新的选项成为可能。
- 可读性和清晰性:选项通过命名函数提供,这使得调用代码更易读和理解。例如,
WithMaxIdleConns(10)比简单的传递一个数字更明确表达了配置的意图。 - 封装性:选项函数内部可以包含复杂的逻辑来设置对象的状态,而这些逻辑对于使用者是隐藏的。这有助于封装配置细节,使得外部代码不需要关心如何正确设置对象状态。
第三方源码(ETCD)
下面是etcd(github.com)的底层源码:
func NewCtxClient(ctx context.Context, opts ...Option) *Client { cctx, cancel := context.WithCancel(ctx) c := &Client{ctx: cctx, cancel: cancel, lgMu: new(sync.RWMutex)} for _, opt := range opts { opt(c) } if c.lg == nil { c.lg = zap.NewNop() } return c}type Option func(*Client)func WithZapLogger(lg *zap.Logger) Option { return func(c *Client) { c.lg = lg }}可以看到使用的是典型的函数选项模式来实现日志的相关设置。
接口选项
Demo 示例
type ClientOptions struct { maxIdleConns int timeout int wait bool}// ClientOption 定义了一个可以应用于ClientOptions的接口type ClientOption interface { apply(*ClientOptions)}// MaxIdleConnsOption 设置最大空闲连接数type MaxIdleConnsOption struct { MaxIdleConns int}// MaxIdleConnsOption 实现ClientOption接口func (m *MaxIdleConnsOption) apply(opts *ClientOptions) { opts.maxIdleConns = m.MaxIdleConns}// WithMaxIdleConns 设置最大空闲连接数func WithMaxIdleConns(maxIdleConns int) *MaxIdleConnsOption { return &MaxIdleConnsOption{MaxIdleConns: maxIdleConns}}// TimeoutOption 设置连接池释放连接的超时连接type TimeoutOption struct { Timeout int}// TimeoutOption 实现ClientOption接口func (t *TimeoutOption) apply(opts *ClientOptions) { opts.timeout = t.Timeout}// WithTimeout 设置连接池释放连接的超时连接func WithTimeout(timeout int) *TimeoutOption { return &TimeoutOption{Timeout: timeout}}// WaitModeOption 设置是否阻塞type WaitModeOption struct { WaitMode bool}// WaitModeOption 实现ClientOption接口func (w *WaitModeOption) apply(opts *ClientOptions) { opts.wait = w.WaitMode}// WithWaitMode 设置是否阻塞func WithWaitMode(wait bool) *WaitModeOption { return &WaitModeOption{WaitMode: wait}}// Client 表示一个Redis客户端type Client struct { network string address string options *ClientOptions}// NewClient 新建客户端, 适用于简单或标准的Redis连接需求func NewClient(network, address string, opts ...interface{}) *Client { if network == "" || address == "" { return nil } options := &ClientOptions{} for _, opt := range opts { switch o := opt.(type) { case ClientOption: o.apply(options) } } return &Client{ network: network, address: address, options: options, }}func main() { client := NewClient("tcp", "localhost:6379", WithMaxIdleConns(10), WithTimeout(300), WithWaitMode(true)) if client == nil { panic("client is nil") } fmt.Println(client.options.maxIdleConns) fmt.Println(client.options.timeout) fmt.Println(client.options.wait)}运行结果如下:
PS D:\GoProjects\main> go run main.go10300true在这段代码当中:
-
ClientOptions是一个简单的结构体,用于存储配置选项。 -
ClientOption接口定义了一个名为apply的方法,该方法接受一个*ClientOptions类型的参数。如果某个类型想要实现该接口,那么必须提供apply方法的具体实现(鸭子模型:如果一个动物实现了鸭子叫的方法,那么就可以认为这个动物是一只鸭子)。这个设计模式的核心思想是:通过接口将修改配置的行为抽象出来,使得不同的配置选项可以以统一的方式应用到ClientOptions实例上。 -
MaxIdleConnsOption是一个结构体,它实现了ClientOption接口。该结构体的字段MaxIdleConns用于存储最大空闲连接数的值。它的apply方法直接修改传入的ClientOptions实例,设置maxIdleConns字段的值。 -
WithMaxIdleConns函数是一个工厂函数,它接受一个整数作为参数,并返回一个ClientOption接口。这个函数构造一个MaxIdleConnsOption实例,并通过该实例的指针返回,该指针满足ClientOption接口的要求。
这种方式通过接口ClientOption实现了多态。在NewClient函数中,它们都能接受一系列ClientOption接口类型的参数,而这些参数实际上可以是任何实现了ClientOption接口的类型。这使得NewClient函数能够以统一的方式处理不同的配置选项,而无需知道配置选项的具体类型和实现细节。
通过这种设计,可以构建一个灵活且易于扩展的配置系统,这在创建需要处理多种配置场景的复杂系统时非常有用。希望这个解释有助于您更好地理解代码的结构和背后的设计理念。
第三方源码(gRPC)
type ServerOption interface { apply(*serverOptions)}type funcServerOption struct { f func(*serverOptions)}// funcServerOption 实现ServerOption接口func (fdo *funcServerOption) apply(do *serverOptions) { fdo.f(do)}// newFuncServerOption 是一个工厂函数。作用是封装任何给定的函数为一个 ServerOption,从而使它可以被用作配置选项。func newFuncServerOption(f func(*serverOptions)) *funcServerOption { return &funcServerOption{ f: f, }}func SharedWriteBuffer(val bool) ServerOption { return newFuncServerOption(func(o *serverOptions) { o.sharedWriteBuffer = val })}func WriteBufferSize(s int) ServerOption { return newFuncServerOption(func(o *serverOptions) { o.writeBufferSize = s })}func ReadBufferSize(s int) ServerOption { return newFuncServerOption(func(o *serverOptions) { o.readBufferSize = s })}这种模式非常适用于需要多个可选配置参数的情况,特别是在对象可能有多种配置组合时。通过使用这种模式,gRPC 确保了其 API 的灵活性和扩展性,同时也提高了代码的维护性和可读性。
总结:
- 函数 Options:适用于需要配置一组相关的参数的情况。
- 接口 Options:适用于需要定义多态行为的情况。