大家好,我是 polarisxu。
Go 不是完全面向對象語言,有一些面向對象模式不太適合它。但經過這些年的發展,Go 有自己的一些模式。今天介紹一個常見的模式:函數式選項模式(Functional Options Pattern)。
01 什么是函數式選項模式
Go 語言沒有構造函數,一般通過定義 New 函數來充當構造函數。然而,如果結構有較多字段,要初始化這些字段,有很多種方式,但有一種方式認為是最好的,這就是函數式選項模式(Functional Options Pattern)。
函數式選項模式是一種在 Go 中構造結構體的模式,它通過設計一組非常有表現力和靈活的 API 來幫助配置和初始化結構體。
在 Uber 的 Go 語言規范中提到了該模式:
Functional options 是一種模式,在該模式中,你可以聲明一個不透明的 Option 類型,該類型在某些內部結構中記錄信息。你接受這些可變數量的選項,并根據內部結構上的選項記錄的完整信息進行操作。
將此模式用于構造函數和其他公共 API 中的可選參數,你預計這些參數需要擴展,尤其是在這些函數上已經有三個或更多參數的情況下。
02 一個示例
為了更好的理解該模式,我們通過一個例子來講解。
定義一個 Server 結構體:
- package main
- type Server {
- host string
- port int
- }
- func New(host string, port int) *Server {
- return &Server{host, port}
- }
- func (s *Server) Start() error {
- }
如何使用呢?
- package main
- import (
- "log"
- "server"
- )
- func main() {
- svr := New("localhost", 1234)
- if err := svr.Start(); err != nil {
- log.Fatal(err)
- }
- }
但如果要擴展 Server 的配置選項,如何做?通常有三種做法:
為每個不同的配置選項聲明一個新的構造函數
定義一個新的 Config 結構體來保存配置信息
使用 Functional Option Pattern
做法 1:為每個不同的配置選項聲明一個新的構造函數
這種做法是為不同選項定義專有的構造函數。假如上面的 Server 增加了兩個字段:
- type Server {
- host string
- port int
- timeout time.Duration
- maxConn int
- }
一般來說,host 和 port 是必須的字段,而 timeout 和 maxConn 是可選的,所以,可以保留原來的構造函數,而這兩個字段給默認值:
- func New(host string, port int) *Server {
- return &Server{host, port, time.Minute, 100}
- }
然后針對 timeout 和 maxConn 額外提供兩個構造函數:
- func NewWithTimeout(host string, port int, timeout time.Duration) *Server {
- return &Server{host, port, timeout}
- }
- func NewWithTimeoutAndMaxConn(host string, port int, timeout time.Duration, maxConn int) *Server {
- return &Server{host, port, timeout, maxConn}
- }
這種方式配置較少且不太會變化的情況,否則每次你需要為新配置創建新的構造函數。在 Go 語言標準庫中,有這種方式的應用。比如 net 包中的 Dial 和 DialTimeout:
- func Dial(network, address string) (Conn, error)
- func DialTimeout(network, address string, timeout time.Duration) (Conn, error)
做法 2:使用專門的配置結構體
這種方式也是很常見的,特別是當配置選項很多時。通常可以創建一個 Config 結構體,其中包含 Server 的所有配置選項。這種做法,即使將來增加更多配置選項,也可以輕松的完成擴展,不會破壞 Server 的 API。
- type Server {
- cfg Config
- }
- type Config struct {
- Host string
- Port int
- Timeout time.Duration
- MaxConn int
- }
- func New(cfg Config) *Server {
- return &Server{cfg}
- }
在使用時,需要先構造 Config 實例,對這個實例,又回到了前面 Server 的問題上,因為增加或刪除選項,需要對 Config 有較大的修改。如果將 Config 中的字段改為私有,可能需要定義 Config 的構造函數。。。
做法 3:使用 Functional Option Pattern
一個更好的解決方案是使用 Functional Option Pattern。
在這個模式中,我們定義一個 Option 函數類型:
- type Option func(*Server)
Option 類型是一個函數類型,它接收一個參數:*Server。然后,Server 的構造函數接收一個 Option 類型的不定參數:
- func New(options ...Option) *Server {
- svr := &Server{}
- for _, f := range options {
- f(svr)
- }
- return svr
- }
那選項如何起作用?需要定義一系列相關返回 Option 的函數:
- 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
- }
- }
針對這種模式,客戶端類似這么使用:
- package main
- import (
- "log"
- "server"
- )
- func main() {
- svr := New(
- WithHost("localhost"),
- WithPort(8080),
- WithTimeout(time.Minute),
- WithMaxConn(120),
- )
- if err := svr.Start(); err != nil {
- log.Fatal(err)
- }
- }
將來增加選項,只需要增加對應的 WithXXX 函數即可。
這種模式,在第三方庫中使用挺多,比如 github.com/gocolly/colly:
- type Collector {
- // 省略...
- }
- func NewCollector(options ...CollectorOption) *Collector
- // 定義了一系列 CollectorOpiton
- type CollectorOption{
- // 省略...
- }
- func AllowURLRevisit() CollectorOption
- func AllowedDomains(domains ...string) CollectorOption
- ...
不過 Uber 的 Go 語言編程規范中提到該模式時,建議定義一個 Option 接口,而不是 Option 函數類型。該 Option 接口有一個未導出的方法,然后通過一個未導出的 options 結構來記錄各選項。
Uber 的這個例子能看懂嗎?
- type options struct {
- cache bool
- logger *zap.Logger
- }
- type Option interface {
- apply(*options)
- }
- type cacheOption bool
- func (c cacheOption) apply(opts *options) {
- opts.cache = bool(c)
- }
- func WithCache(c bool) Option {
- return cacheOption(c)
- }
- type loggerOption struct {
- Log *zap.Logger
- }
- func (l loggerOption) apply(opts *options) {
- opts.logger = l.Log
- }
- func WithLogger(log *zap.Logger) Option {
- return loggerOption{Log: log}
- }
- // Open creates a connection.
- func Open(
- addr string,
- opts ...Option,
- ) (*Connection, error) {
- options := options{
- cache: defaultCache,
- logger: zap.NewNop(),
- }
- for _, o := range opts {
- o.apply(&options)
- }
- // ...
- }
03 總結
在實際項目中,當你要處理的選項比較多,或者處理不同來源的選項(來自文件、來自環境變量等)時,可以考慮試試函數式選項模式。
注意,在實際工作中,我們不應該教條的應用上面的模式,就像 Uber 中的例子,Open 函數并非只接受一個 Option 不定參數,因為 addr 參數是必須的。因此,函數式選項模式更多應該應用在那些配置較多,且有可選參數的情況。
參考文獻
https://golang.cafe/blog/golang-functional-options-pattern.html
https://github.com/uber-go/guide/blob/master/style.md#functional-options
原文鏈接:https://mp.weixin.qq.com/s/B-HZu1oZGseaOuNUznjJFA