引言
学习 golang 不久后,因工作须要接触到了 go-micro 这一微服务框架。通过读源码,写业务代码,定制个性化插件,解决框架问题这些过程后,对它有了更粗浅的了解。总的来说,这是一个性能较为齐全,形象较为正当的微服务框架,非常适合用来强化 golang 的学习以及加深对微服务畛域常识的了解,但是否达到了生产规范的要求至今仍是个未知数,须要更多的测验。
本系列文章基于 asim/go-micro v3.5.2 版本,读者可于 https://github.com/asim/go-micro 拉取源代码进行学习。
筹备
抛开微服务的畛域常识,go-micro 的整体设计次要基于 Functional Options 以及 Interface Oriented,把握这两点基本上就把握住了它的代码格调,对于之后的学习、应用、扩大大有裨益。因而首先介绍这两种设计模式,为之后的深刻了解做好铺垫。
Functional Options
一. 问题引入
在介绍 Functional Options 之前,咱们先来思考一个平时编程的惯例操作:配置并初始化一个对象。例如生成一个 Server 对象,须要指明 IP 地址和端口,如下所示:
type Server struct {
Addr string
Port int
}
很天然的,构造函数能够写成如下模式:
func NewServer(addr string, port int) (*Server, error) {
return &Server{
Addr: addr,
Port: port,
}, nil
}
这个构造函数简略间接,但事实中一个 Server 对象远不止 Addr 和 Port 两个属性,为了反对更多的性能,例如监听 tcp 或者 udp,设置超时工夫,限度最大连接数,须要引入更多的属性,此时 Server 对象变成了如下模式:
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
}
为了投合属性变动,构造函数会变成以下模式:
func NewServer(addr string, port int, protocol string, timeout time.Duration, maxConns int) (*Server, error) {
return &Server{
Addr: addr,
Port: port,
Protocol: protocol,
Timeout: timeout,
MaxConns: maxConns,
}, nil
}
置信大家曾经发现了,随着属性的增多,这种模式的构造函数会越来越长,最初臃肿不堪。如果这个对象只是本人开发和应用,把控好具体细节和复杂度,还是能承受的,只须要将参数换行就行。但如果这个对象作为库的公共成员被其余开发者应用,那么这个构造函数即 API 会带来以下问题:
- 使用者只从 API 签名无奈判断哪些参数是必须的,哪些参数是可选的
例如 NewServer 被设计为必须传入 addr 与 port,默认设置为监听 tcp,超时工夫 60s,最大连接数 10000。在没有文档的状况下,当初一个使用者想疾速应用这个 API 搭建 demo,凭借教训他会传入 addr 和 port,但对于其它参数,他是无能为力的。比方 timeout 传 0 是意味着采纳默认值还是永不超时,还是说必须要传入无效的值才行,根本无法从 API 签名得悉。此时使用者只能通过查看具体实现能力把握 API 正确的用法。 - 减少或删除 Server 属性后,API 大概率也会随之变动
当初思考到平安属性,须要反对 TLS,那么 API 签名会变成如下模式并使得应用之前版本 API 的代码生效:
func NewServer(addr string, port int, protocol string, timeout time.Duration, maxConns int, tls *tls.Config) (*Server, error)
能够看到,这种 API 写法非常简略,但它把所有的复杂度都裸露给了使用者,蹩脚的文档阐明更是让这种状况雪上加霜。因而 API 尽量不要采纳这种模式书写,除非能够确定这个对象非常简单且稳固。
二. 解决方案
为了升高复杂度,缩小使用者的心智累赘,有一些解决方案可供参考。
1. 伪重载函数
golang 自身不反对函数的重载,为了达到相似的成果,只能结构多个 API。每个 API 都带有残缺的必填参数和局部选填参数,因而 API 的数量即为选填参数的排列组合。具体签名如下所示(未展现全副):
func NewServer(addr string, port int) (*Server, error)
func NewServerWithProtocol(addr string, port int, protocol string) (*Server, error)
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error)
func NewServerWithProtocolAndTimeout(addr string, port int, protocol string, timeout time.Duration) (*Server, error)
相比于单个 API 蕴含所有参数,这种形式能够让使用者分清必填参数和可选参数,依据需要抉择适合的 API 进行调用。然而,随着参数越来越多,API 的数量也会随之收缩,因而这并不是一个优雅的解决方案,只有在对象简略且稳固的状况下才举荐应用。
2. 结构配置对象
比拟通用的解决方案就是结构一个 Config 对象蕴含所有参数或者可选参数,置信大家在各大开源库中也能见到这种形式。在本例中,相应的构造体和 API 如下所示:
type Config struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
}
func NewServer(c *Config) (*Server, error)
type Config struct {
Protocol string
Timeout time.Duration
MaxConns int
}
func NewServer(addr string, port int, c *Config) (*Server, error)
前一种形式须要文档阐明必填参数和可选参数,但益处是 API 显得很清新,库中的其它结构对象 API 都能以这种形式进行编写,达到格调的对立。后一种形式通过 API 就能分清必填参数和可选参数,但每个对象的必填参数不尽相同,因而库中的结构对象 API 不能达到格调的对立。
这两种形式都能较好地应答参数的变动,减少参数并不会让应用之前 API 版本的代码生效,且可通过传零值的形式来应用对应可选参数的默认值。在开源库中大部分采纳前一种形式,对 Config 对象做对立的文档形容,补救了无奈间接辨别必填参数和可选参数的毛病,例如 go-redis/redis 中的配置对象(取名 Options,原理雷同):
// Options keeps the settings to setup redis connection.
type Options struct {
// The network type, either tcp or unix.
// Default is tcp.
Network string
// host:port address.
Addr string
// Dialer creates new network connection and has priority over
// Network and Addr options.
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
// Hook that is called when new connection is established.
OnConnect func(ctx context.Context, cn *Conn) error
// Use the specified Username to authenticate the current connection
// with one of the connections defined in the ACL list when connecting
// to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
Username string
// Optional password. Must match the password specified in the
// requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower),
// or the User Password when connecting to a Redis 6.0 instance, or greater,
// that is using the Redis ACL system.
Password string
// Database to be selected after connecting to the server.
DB int
// Maximum number of retries before giving up.
// Default is 3 retries; -1 (not 0) disables retries.
MaxRetries int
// Minimum backoff between each retry.
// Default is 8 milliseconds; -1 disables backoff.
MinRetryBackoff time.Duration
// Maximum backoff between each retry.
// Default is 512 milliseconds; -1 disables backoff.
MaxRetryBackoff time.Duration
// Dial timeout for establishing new connections.
// Default is 5 seconds.
DialTimeout time.Duration
// Timeout for socket reads. If reached, commands will fail
// with a timeout instead of blocking. Use value -1 for no timeout and 0 for default.
// Default is 3 seconds.
ReadTimeout time.Duration
// Timeout for socket writes. If reached, commands will fail
// with a timeout instead of blocking.
// Default is ReadTimeout.
WriteTimeout time.Duration
// Type of connection pool.
// true for FIFO pool, false for LIFO pool.
// Note that fifo has higher overhead compared to lifo.
PoolFIFO bool
// Maximum number of socket connections.
// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
PoolSize int
// Minimum number of idle connections which is useful when establishing
// new connection is slow.
MinIdleConns int
// Connection age at which client retires (closes) the connection.
// Default is to not close aged connections.
MaxConnAge time.Duration
// Amount of time client waits for connection if all connections
// are busy before returning an error.
// Default is ReadTimeout + 1 second.
PoolTimeout time.Duration
// Amount of time after which client closes idle connections.
// Should be less than server's timeout.
// Default is 5 minutes. -1 disables idle timeout check.
IdleTimeout time.Duration
// Frequency of idle checks made by idle connections reaper.
// Default is 1 minute. -1 disables idle connections reaper,
// but idle connections are still discarded by the client
// if IdleTimeout is set.
IdleCheckFrequency time.Duration
// Enables read only queries on slave nodes.
readOnly bool
// TLS Config to use. When set TLS will be negotiated.
TLSConfig *tls.Config
// Limiter interface used to implemented circuit breaker or rate limiter.
Limiter Limiter
}
func NewClient(opt *Options) *Client
但这种形式并不是完满的,一是参数传零值表明应用默认值的形式打消了零值本来的含意,例如 timeout 设为 0 示意应用默认值 60s,但 0 自身可示意永不超时;二是在只有可选参数没有必填参数的状况下,使用者只想应用默认值,但到底是传入 nil 还是 &Config{}也会让他们有点摸不着头脑,更重要的是,使用者可能并不想传入任何参数,但迫于 API 的模式必须传入;三是传入 Config 指针无奈确定 API 是否会对 Config 对象做批改,因而无奈复用 Config 对象。
总的来说,这种形式搭配具体的文档能够起到较好的成果,API 变得清新,扩展性较好,但还是向使用者残缺地裸露了复杂度。如果残缺地把握各个参数的行为与意义是很有必要的,那么这种形式会非常适合。
3. Builder 模式
Builder 是一种经典设计模式,用来组装具备简单构造的实例。在 java 中,Server 对象应用 Builder 模式进行构建根本如下所示:
Server server = new Server.Builder("127.0.0.1", 8080)
.MaxConnections(10000)
.Build();
必填参数在 Builder 办法中传入,可选参数通过链式调用的形式增加,最初通过 Build 办法生成实例。java 中的具体实现办法可参考 https://www.jianshu.com/p/e2a…,这里不再赘述。
当初来讲讲怎么用 golang 实现 Builder 模式,先上代码:
type ServerBuilder struct {
s *Server
err error
}
// 首先应用必填参数进行结构
func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
// 对参数进行验证,若呈现谬误则设置 sb.err = err,这里假如没有谬误,后续同理
sb.s = &Server{}
sb.s.Addr = addr
sb.s.Port = port
return sb
}
func (sb *ServerBuilder) Protocol(protocol string) *ServerBuilder {
if sb.err == nil {sb.s.protocol = protocol}
return sb
}
func (sb *ServerBuilder) Timeout(timeout time.Duration) *ServerBuilder {
if sb.err == nil {sb.s.Timeout = timeout}
return sb
}
func (sb *ServerBuilder) MaxConns(maxConns int) *ServerBuilder {
if sb.err == nil {sb.s.MaxConns = maxConns}
return sb
}
func (sb *ServerBuilder) Build() (*Server, error) {return sb.s, sb.err}
因为 golang 应用返回谬误值的形式来处理错误,所以要实现链式函数调用并兼顾错误处理就须要应用 ServerBuilder 来包装 Server 与 error,每次设置可选参数前都检查一下 ServerBuilder 中的 error。当然间接解决 Server 也是没问题的,但就须要在 Server 中加上 error 成员,这样做净化了 Server,不太举荐。应用这种解决方案生成 Server 对象如下所示:
sb := ServerBuilder()
srv, err := sb.Create("127.0.0.1", 8080)
.Protocol("tcp")
.MaxConns(10000)
.Build()
这种形式通过 Create 函数即可确定必填参数,通过 ServerBuilder 的其它办法就能够确定可选参数,且扩展性好,增加一个可选参数后在 ServerBuilder 中增加一个办法并在调用该 API 处再加一行链式调用即可。
因为读的代码还是太少(持续滚去读了),没有找到残缺应用 Builder 模式的出名开源库((lll¬ω¬)),比拟显著的能够参考 https://github.com/uber-go/za…,但 zap 混用了另外一种模式,也是最初要介绍的模式,所以看完本文的残缺介绍后再看 zap 的实现比拟得当。
(这里写个小认识,想间接看注释的能够跳过。逛知乎看了很多 golang 和 java 卫道者的骂战,谁比谁牛逼暂无定论,但界线是肯定要划清的。具体体现在 gopher 对依赖注入,设计模式等在 java 大行其道的形式十分抵制,认为不合乎 golang 大道至简的设计思维。而 javaer 则讥嘲 golang 过于《大道至简》了。所以斗胆猜想誓要划清界限的 gopher 不想应用 Builder 模式,至多不能明火执仗地应用)。
4. Functional Options
铺垫了这么久,终于来到了重头戏 Functional Options。回顾一下之前的解决方案,除开政治不正确的 Builder 模式,总结一下对 API 的需要为:
①API 应尽量具备自描述性,只需看函数签名就能够不便辨别必填参数和选填参数
②反对默认值
③在只想应用默认参数的状况下,不须要传入 nil 或者空对象这种令人蛊惑的参数