注:本文的灵感来源于GOPHER 2020年大会陈皓的分享,原PPT的链接 可能并不方便获取,所以我下载了一份PDF 到git仓,方便大家阅读。我将结合自己的实际项目经历,与大家一起细品这份文档。
目录
ServerConfig 我们先来看看一个常见的HTTP服务器的配置,它区分了2个必填参数与4个非必填参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type ServerCfg struct { Addr string Port int Protocol string Timeout time.Duration MaxConns int TLS *tls.Config } func NewServer (addr string , port int ) (*Server, error) {}func NewTLSServer (addr string , port int , tls *tls.Config) (*Server, error) {}func NewServerWithTimeout (addr string , port int , timeout time.Duration) (*Server, error) {}func NewTLSServerWithMaxConnAndTimeout (addr string , port int , maxconns int , timeout time.Duration, tls *tls.Config) (*Server, error) {}
SplitConfig 编程的一大重点,就是要 分离变化点和不变点
。这里,我们可以将必填项认为是不变点,而非必填则是变化点。
我们将非必填的选项拆分出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type Config struct { Protocol string Timeout time.Duration MaxConns int TLS *tls.Config } type Server struct { Addr string Port int Conf *Config } func NewServer (addr string , port int , conf *Config) (*Server, error) { return &Server{ Addr: addr, Port: port, Conf: conf, }, nil } func main () { srv1, _ := NewServer("localhost" , 9000 , nil ) conf := Config{Protocol: "tcp" , Timeout: 60 * time.Second} srv2, _ := NewServer("localhost" , 9000 , &conf) fmt.Println(srv1, srv2) }
到这里,其实已经满足大部分的开发需求了。那么,我们将进入今天的重点。
Functional Option 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 type Server struct { Addr string Port int Protocol string Timeout time.Duration MaxConns int TLS *tls.Config } type Option func (*Server) func Protocol (p string ) Option { return func (s *Server) { s.Protocol = p } } func Timeout (timeout time.Duration) Option { return func (s *Server) { s.Timeout = timeout } } func MaxConns (maxconns int ) Option { return func (s *Server) { s.MaxConns = maxconns } } func TLS (tls *tls.Config) Option { return func (s *Server) { s.TLS = tls } } func NewServer (addr string , port int , options ...Option) (*Server, error) { srv := Server{ Addr: addr, Port: port, Protocol: "tcp" , Timeout: 30 * time.Second, MaxConns: 1000 , TLS: nil , } for _, option := range options { option(&srv) } return &srv, nil } func main () { s1, _ := NewServer("localhost" , 1024 ) s2, _ := NewServer("localhost" , 2048 , Protocol("udp" )) s3, _ := NewServer("0.0.0.0" , 8080 , Timeout(300 *time.Second), MaxConns(1000 )) fmt.Println(s1, s2, s3) }
耗子哥给出了6个点,但我感受最深的是以下两点:
可读性强,将配置都转化成了对应的函数项option
扩展性好,新增参数只需要增加一个对应的方法
那么对应的代价呢?就是需要编写多个Option函数,代码量会有所增加。
如果大家对这个感兴趣,可以去看一下Rob Pike的这篇blog 。
Further 顺着耗子叔的例子,我们再思考一下,如果配置的过程中有参数限制,那么我们该怎么办呢?
首先,我们改造一下函数Option
1 2 type OptionWithError func (*Server) error
然后,我们改造一下其中两个函数作为示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func Protocol (p string ) OptionWithError { return func (s *Server) error { if p == "" { return errors.New("empty protocol" ) } s.Protocol = p return nil } } func Timeout (timeout time.Duration) Option { return func (s *Server) error { if timeout.Seconds() < 1 { return errors.New("time out should not less than 1s" ) } s.Timeout = timeout return nil } }
我们再做一次改造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func NewServer (addr string , port int , options ...OptionWithError) (*Server, error) { srv := Server{ Addr: addr, Port: port, Protocol: "tcp" , Timeout: 30 * time.Second, MaxConns: 1000 , TLS: nil , } for _, option := range options { if err := option(&srv); err != nil { return nil , err } } return &srv, nil }
改造基本到此完成,希望能给大家带来一定的帮助。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili:https://space.bilibili.com/293775192
公众号:golangcoding