go 编程模式之 functional options | go 技术论坛-金年会app官方网
什么是functional options编程模式
functional options 编程模式是一种在 go 语言中构造结构体的模式,这种模式允许用户通过一系列函数来传递配置选项,而不是通过构造函数的参数列表。它在 go 中特别有用,因为 go 没有构造函数,通常通过定义new
函数来初始化结构体。
为什么使用functional options模式
在 go 中,如果一个结构体有很多配置选项,传统的方法是为每个不同的配置选项声明一个新的构造函数,或者定义一个新的配置结构体来保存配置信息。但是,这些方法都有局限性,比如增加新的配置选项时需要修改构造函数或配置结构体,这可能会导致代码的维护成本增加。functional options 模式提供了一种更灵活和可扩展的方式来处理这种情况。
实例初始化
在介绍 functional options 模式之前,先来看一下传统的 struct 实例初始化是怎么做的。
假设我们有一个 server 结构体如下:
type server struct {
host string
port int
timeout time.duration
maxconn int
}
为了在实例化时更好的控制结构体的初始值,通常需要编写构造函数来实现:
func newserver() *server {
return &server{
host: "127.0.0.1",
port: 8080,
timeout: time.second * 5,
maxconn: 1000,
}
}
func newcustomserver(host string, port int) *server {
return &server{
host: host,
port: port,
}
}
由于 go 语言不支持重载函数,所以,创建两种 server 配置就意味着构造两个不同函数名的函数,随着业务的扩展,可能会出现更多不同的 server 配置,就需要创建更多 server 构造函数来满足要求。
为了解决这个问题,通常有以下几种解决方法。
方法一:编写 set 方法
func (s *server) sethost(host string) {
s.host = host
}
func (s *server) setport(port int) {
s.port = port
}
func (s *server) settimeout(timeout time.duration) {
s.timeout = timeout
}
func (s *server) setmaxconn(maxconn int) {
s.maxconn = maxconn
}
为 server 结构体的每个字段写一个 set 方法,这样我们在初始化实例之后,可以调用对应的 set 方法来为我们想要的字段进行赋值。
func setupserver() {
s := newcustomserver("192.168.1.1", 8081)
s.settimeout(time.second * 10)
s.setmaxconn(2000)
fmt.println(s)
}
这种方式可以直接地表达我们的意图,即调用一个方法设置一个属性。但缺点是需要创建很多的 set 方法,实例化时也需要很多的行来设置多个属性,所以当你的实例初始化参数比较复杂时,不推荐此种方式。
方法二:使用结构体来传递可选参数
我们把 host 和 port 定义为 server 的必传参数,其他为可选参数,将其他的可选参数用一个新的结构体 config 来表示,然后将 config 嵌入到 server 结构体中。
type server struct {
host string
port int
conf *config
}
type config struct {
maxconn int
timeout time.duration
}
这时候我们的构造函数为下面这样:
func newserver(host string, port int, conf *config) *server {
if conf == nil {
conf = &config{
maxconn: 1000,
timeout: time.second * 5,
}
}
return &server{
host: host,
port: port,
conf: conf,
}
}
在进行初始化的时候需要先构造一个 config 对象,然后传递给构造函数。
func setupserver() {
conf := &config{
maxconn: 2000,
timeout: time.second * 10,
}
// 默认配置
s1 := newserver("192.168.1.1", 8081, nil)
// 自定义配置
s2 := newserver("192.168.1.2", 8082, conf)
}
这种方式使用 config 结构体来存放可选参数,保持了 api 简洁,提高了代码的可读性和可维护性,同时提供了良好的扩展性。但缺点是稍微复杂了一点,也不那么美观,对于默认的配置来说,需要多一个config 参数。另外,需要注意nil
和config{}
的区别,虽然我们在构造函数中处理了nil
的情况,但仍然存在外部代码误用的风险,例如忘记初始化 config 或者错误地传递了nil
值。
方法三 builder 模式
学习过设计模式的人,肯定还会想到使用 builder 模式。这种方式允许我们使用链式调用的方法来初始化实例。仿照 builder 模式,把我们的代码修改为下面的样子:
type server struct {
host string
port int
maxconn int
timeout time.duration
}
func newserver(host string, port int) *server {
return &server{
host: host,
port: port,
}
}
func (s *server) withmaxconn(maxconn int) *server {
s.maxconn = maxconn
return s
}
func (s *server) withtimeout(timeout time.duration) *server {
s.timeout = timeout
return s
}
这样,我们就可以使用下面的方式来初始化代码了:
func setupserver() {
s := newserver("192.168.1.1", 8081).
withmaxconn(1000).
withtimeout(time.second * 10)
}
注意,这里没有考虑错误处理的情况。如果自定义参数不多,参数也都相对比较明确的情况下是可以不考虑错误处理的。但如果参数比较复杂,推荐使用一个包装类,这样最后在处理参数错误的时候会方便很多。大概代码像下面这样:
package options
import (
"crypto/tls"
"errors"
"fmt"
"time"
)
// server 定义了服务器的结构。
type server struct {
addr string
port int
protocol string
maxconns int
timeout time.duration
tls *tls.config // 假设这里有一个 tls.config 类型,用于配置 tls
}
// serverbuilder 是用于构建 server 的建造者。
type serverbuilder struct {
server *server
err error
}
// newserverbuilder 创建一个新的 serverbuilder 实例。
func newserverbuilder() *serverbuilder {
return &serverbuilder{}
}
// withaddr 设置地址,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withaddr(addr string) *serverbuilder {
if addr == "" {
sb.err = errors.new("address is required")
return sb
}
sb.server.addr = addr
return sb
}
// withport 设置端口,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withport(port int) *serverbuilder {
if port <= 0 || port > 65535 {
sb.err = errors.new("invalid port number, must be between 1 and 65535")
return sb
}
sb.server.port = port
return sb
}
// withprotocol 设置协议,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withprotocol(protocol string) *serverbuilder {
if protocol != "tcp" && protocol != "udp" {
sb.err = errors.new("invalid protocol, must be 'tcp' or 'udp'")
return sb
}
sb.server.protocol = protocol
return sb
}
// withmaxconns 设置最大连接数,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withmaxconns(maxconns int) *serverbuilder {
if maxconns < 0 {
sb.err = errors.new("max connections cannot be negative")
return sb
}
sb.server.maxconns = maxconns
return sb
}
// withtimeout 设置超时时间,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withtimeout(timeout time.duration) *serverbuilder {
if timeout < 0 {
sb.err = errors.new("timeout cannot be negative")
return sb
}
sb.server.timeout = timeout
return sb
}
// withtls 设置 tls 配置,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withtls(tlsconfig *tls.config) *serverbuilder {
sb.server.tls = tlsconfig
return sb
}
// build 尝试构建 server 实例。
func (sb *serverbuilder) build() (*server, error) {
if sb.err != nil {
return nil, sb.err
}
return sb.server, nil
}
func setupserver() {
builder := newserverbuilder()
server, err := builder.
withaddr("127.0.0.1").
withport(8080).
withprotocol("udp").
withmaxconns(1024).
withtimeout(30 * time.second).
build()
if err != nil {
fmt.printf("error creating server: %v\n", err)
return
}
fmt.printf("server created: % v\n", server)
}
functional options模式实现
介绍完上面三中实现方式,终于轮到主角 functional options 登场了。基础结构体还是不变:
type server struct {
host string
port int
timeout time.duration
maxconn int
}
创建选项类型和选项函数
为了支持 functional options 模式,我们需要定义一个接受*server
指针的函数类型option
,并创建设置server
属性的选项函数:
type option func(*server)
func withhost(host string) option {
return func(s *server) {
s.host = host
}
}
func withport(port int) option {
return func(s *server) {
s.port = port
}
}
func withtimeout(timeout time.duration) option {
return func(s *server) {
s.timeout = timeout
}
}
func withmaxconn(maxconn int) option {
return func(s *server) {
s.maxconn = maxconn
}
}
定义构造函数和实例化
默认构造函数,使用可选参数,参数类型为我们自定义的 option。接下来是关键点,在构造函数中,使用一个 for 循环,遍历传入的选项函数,完成实例化构造:
func newserver(options ...option) *server {
svr := &server{
host: "localhost",
port: 8080,
timeout: time.minute,
maxconn: 100,
}
for _, opt := range options {
opt(svr)
}
return svr
}
这样在实例化的时候,就可以这样使用:
func setupserver() {
svr := newserver(
withhost("localhost"),
withport(8080),
withtimeout(time.minute),
withmaxconn(120),
)
}
functional options 模式在处理复杂配置选项时是非常有用的,尤其是在这些选项可能来自不同来源(如文件、环境变量等)的情况下,或者更具外部情况决定要开启什么配置的时候。
options 模式最佳实践
- 默认值初始化:在
new
方法中设置默认值,用户可以仅覆盖需要的部分。
func newserver(opts ...option) *server {
server := &server{
host: "localhost", // 默认值
port: 80,
timeout: 30,
maxconnections: 100,
}
for _, opt := range opts {
opt(server)
}
return server
}
// 使用方式
server := newserver(withport(8080))
好处:开发者不需要显式传递所有参数,默认值会自动填充未指定的字段。
- 组合 options:将多个逻辑相关的配置合并为一个复合选项。
func withhighperformancesettings() option {
return func(s *server) {
s.timeout = 10
s.maxconnections = 1000
}
}
// 使用方式
server := newserver(withport(8080), withhighperformancesettings())
好处:提高代码复用性,让复杂配置简单化。
- 条件选项:根据某些条件动态设置参数。
func withconditionaltimeout(condition bool, timeout int) option {
return func(s *server) {
if condition {
s.timeout = timeout
}
}
}
// 使用方式
istest := true
server := newserver(withconditionaltimeout(istest, 5))
好处:通过条件选项可以处理上下文敏感的逻辑。
- 链式 option:支持链式调用,用更简洁的语法配置对象。
func (s *server) apply(opts ...option) *server {
for _, opt := range opts {
opt(s)
}
return s
}
// 使用方式
server := (&server{}).apply(withport(8080), withtimeout(20))
好处:允许对已经创建的对象动态调整配置。
- 类型安全的泛型 option:通过泛型封装更类型安全的选项:
type configurable[t any] struct {
value t
}
type option[t any] func(*configurable[t])
func withvalue[t any](value t) option[t] {
return func(c *configurable[t]) {
c.value = value
}
}
func newconfigurable[t any](opts ...option[t]) *configurable[t] {
c := &configurable[t]{}
for _, opt := range opts {
opt(c)
}
return c
}
// 使用方式
config := newconfigurable[int](withvalue(42))
fmt.println(config.value) // 输出 42
好处:让 option 模式支持更加复杂的类型和配置。
- 调试友好的 option:为每个
option
添加日志或注释,便于排查问题。
func withloggedport(port int) option {
return func(s *server) {
fmt.printf("setting port to %d\n", port)
s.port = port
}
}
好处:对复杂配置进行跟踪和调试。
- 互斥或依赖的 option:在
option
中处理参数间的依赖关系或互斥逻辑。
func withsecuremode(enable bool) option {
return func(s *server) {
if enable {
s.port = 443
s.host = "https"
}
}
}
// 使用方式
server := newserver(withsecuremode(true))
好处:避免调用者在使用时混淆配置规则。
- 延迟计算的 option:某些参数可以在构造对象时动态计算,而不是直接传递。
func withdynamicport(getport func() int) option {
return func(s *server) {
s.port = getport()
}
}
// 使用方式
server := newserver(withdynamicport(func() int { return 8080 }))
好处:支持运行时动态设置值,适合依赖外部条件的场景。
- option 校验:在
new
方法中校验option
的合法性,避免不一致的配置。
func newserver(opts ...option) (*server, error) {
server := &server{}
for _, opt := range opts {
opt(server)
}
if server.port == 0 {
return nil, fmt.errorf("port must be specified")
}
return server, nil
}
好处:在初始化时捕获潜在错误。
总结
functional options 模式增强了代码的灵活性和可维护性,尤其在处理复杂的配置选项时表现尤为突出。本文旨在介绍 functional options 模式的概念及其应用,并讨论了几种常见的实例初始化方法。需强调的是,本文无意贬低或推崇任一特定方法,只是建议在面对复杂的实例化需求时,可以考虑采用 functional options 模式作为金年会app官方网的解决方案之一。选择最适合当前场景的方法,才是正确的打开方式
本作品采用《cc 协议》,转载必须注明作者和本文链接