我查阅了另一种称呼:Functional Options,用来替代函数的展开传参。举例来说,一个函数如果声明超过 4 个参数,就会显得函数声明特别长。当然,也可以声明一个结构体参数,把函数参数当做结构体的字段,这样看起来会精简很多。而 Functional Options 其实也是这种方式的扩展而已。

之前阅读了文章 Functional Options in Go: Implementing the Options Pattern in Golang 之后,感觉内容很实在,推荐大家阅读原文。有时候感觉外语的文章挺实在的,讲的都比较详细。希望不是自己的外语水平有限,看的都是一些新手文章…

假设我们需要提供一个获取 “笔记信息” 的接口,它包括内容、阅读数、作者、创建时间、标签、分类等等信息。但并不是所有的调用方都需要全量的笔记信息,有的调用方可能只需要笔记的作者信息就可以。一般来说,我们会如何处理这种情况呢?如果我们要新返回一个元数据信息,比如是否私密(Private),又该如何处理呢?

函数参数方式

GetV1 的扩展性很差,如果这个接口在项目中使用的地方特别多,要新加一个 withPrivate 的参数,之前所有调用 GetV1 的方法都需要加这个参数。GetV2 是 GetV1 的另一种写法,并没有带来任何优势,甚至还引入了一个劣势:不能一眼看出方法提供了哪些选项。

函数参数声明扩展个数是一个问题,其实,参数赋值也可能是一个问题。如果调用链特别深的话,要将实参从调用链的头部一直传递到尾部,可能会横跨多个别的函数调用。当然,这不是我们讲解的重点。

type NoteClient struct {
}

// func1 通过明确的参数来控制加载的数据
func (cli *NoteClient) GetV1(ctx context.Context, 
	withContent, withView, vithAuthor, withCreateTime bool) (*NoteInfo, error) {
}

// func2 通过扩展的参数来控制
func (cli *NoteClient) GetV2(ctx context.Context, withOptions ...bool) (*NoteInfo, error) {
}

扩展结构体字段

因为函数的参数声明都是独立展开的,所以参数扩展起来只会导致声明越来越长。但我们可以认为的将参数聚合起来,目前可以参数两种聚合的形式:一种是将参数声明为接受结构体的字段,这样每次实例化结构的时候,先将参数做好初始化;另一种是将参数独立为一个入参结构体对象。

虽然都是参数结构体封装的形式,但这两个结构体的含义确实不相同。从类的角度来说,第一种是扩展了类的成员属性,第二种只是声明了一个结构体入参。现在,如果要扩展是否私密,只需要在结构体中追加新的字段 WithPrivate。

type NoteClient struct {
	WithContent bool
	WithView 	bool
	WithAuthor	bool
}

// 或者单独声明一个参数结构体
type NoteOption struct {
	WithContent bool
	WithView 	bool
	WithAuthor	bool
}

// func1 在结构体中加扩展字段
func (cli *NoteClient) GetV1(ctx context.Context) (*NoteInfo, error) {
}

// func2 通过一个结构体 withOption 参数来控制
func (cli *NoteClinet) GetV2(ctx context.Context, withOption *NoteOption) (*NoteInfo, error) {
}

定义函数选项

NoteOptionFunc

可以看出,这其实是使用了闭包的特性。以 WithContent 函数为例,传递了形参 isCnt,在返回函数内部给 opt 做赋值。这个过程,isCnt 可以理解为是通过引用传递的,外层参数的修改也会影响到内存的元素。不过,这个例子没有体现出这个特性来。

type NoteClient struct {
	WithContent bool
	WithView 	bool
	WithAuthor	bool
}

type NoteOptionFunc func(*NoteClient)

func WithContent(isCnt bool) NoteOptionFunc {
	return func(opt *NoteClient) {
		opt.WithContent = isCnt
	}
}

func WithNoContent() NoteOptionFunc {
	return func(opt *NoteClient) {
		opt.WithContent = false
	}
}

func NewNoteClient(opts ...NoteOptionFunc) *NoteClient {
	cli := &NoteClient{}
	for _, f := range opts {
		f(cli)
	}
	return cli
}

这种模式在很多地方被称为:最佳实践。不过,我个人觉得有利有弊,也不能所有的参数赋值都使用 Functional Options 的模式,这种模式的缺点就是代码量多,优点是赋值过程非常明确,扩展性强,比较适用于底层中间件之类参数赋值操作。