Functional options and config for APIs

Redis client example
// DialTimeout acts like Dial for establishing the
// connection to the server, writing a command and reading a reply.
func Dial(network, address string) (Conn, error)
“我要自定义超时时间!”
“我要设定 Database!”
“我要控制连接池的策略!”
“我要安全使用Redis,让我填一下 Password!”
“可以提供一下慢查询请求记录,并且可以设置 slowlog 时间?”

展开查看详情

1.Functional options and config for APIs ⽑剑@bilibili

2.Redis client example // DialTimeout acts like Dial for establishing the // connection to the server, writing a command and reading a reply. func Dial(network, address string) (Conn, error) “我要自定义超时时间!” “我要设定 Database!” “我要控制连接池的策略!” “我要安全使用Redis,让我填一下 Password!” “可以提供一下慢查询请求记录,并且可以设置 slowlog 时间?”

3.Features // DialTimeout acts like Dial for establishing the // connection to the server, writing a command and reading a reply. func Dial(network, address string) (Conn, error) // DialTimeout acts like Dial but takes timeouts for establishing the // connection to the server, writing a command and reading a reply. func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) // DialDatabase acts like Dial but takes database for establishing the // connection to the server, writing a command and reading a reply. func DialDatabase(network, address string, database int) (Conn, error) // DialPool func DialPool... // ...

4.net/http package main import ( "log" "net/http" "time" ) func main() { s := &http.Server{ Addr: ":8080", Handler: nil, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } log.Fatal(s.ListenAndServe()) }

5.Configuration struct API // Config redis settings. type Config struct { *pool.Config Addr string Auth string DialTimeout time.Duration ReadTimeout time.Duration WriteTimeout time.Duration } // NewConn new a redis conn. func NewConn(c *Config) (cn Conn, err error) func main() { c := &redis.Config{ Addr: "tcp://127.0.0.1:3389", } r, _ := redis.NewConn(c) c.Addr = "tcp://127.0.0.1:3390" // 副作用是什么? }

6.Configuration struct API // NewConn new a redis conn. func NewConn(c Config) (cn Conn, err error) // NewConn new a redis conn. func NewConn(c *Config) (cn Conn, err error) // NewConn new a redis conn. func NewConn(c ...*Config) (cn Conn, err error) import ( "github.com/go-kratos/kratos/pkg/log" ) func main() { log.Init(nil) // 这样使用默认配置 // config.fix() // 修正默认配置 } “I believe that we, as Go programmers, should work hard to ensure that nil is never a parameter that needs to be passed to any public function.” – Dave Cheney

7.Functional options Self-referential functions and the design of options -- Rob Pike Functional options for friendly APIs -- Dave Cheney // DialOption specifies an option for dialing a Redis server. type DialOption struct { f func(*dialOptions) } // Dial connects to the Redis server at the given network and // address using the specified options. func Dial(network, address string, options ...DialOption) (Conn, error) { do := dialOptions{ dial: net.Dial, } for _, option := range options { option.f(&do) } // ... }

8.Functional options package main import ( "time" "github.com/go-kratos/kratos/pkg/cache/redis" ) func main() { c, _ := redis.Dial("tcp", "127.0.0.1:3389", redis.DialDatabase(0), redis.DialPassword("hello"), redis.DialReadTimeout(10*time.Second)) }

9.Functional options // DialOption specifies an option for dialing a Redis server. type DialOption func(*dialOptions) // Dial connects to the Redis server at the given network and // address using the specified options. func Dial(network, address string, options ...DialOption) (Conn, error) { do := dialOptions{ dial: net.Dial, } for _, option := range options { option(&do) } // ... }

10.Functional options type option func(f *Foo) option // Verbosity sets Foo's verbosity level to v. func Verbosity(v int) option { return func(f *Foo) option { prev := f.verbosity f.verbosity = v return Verbosity(prev) } } func DoSomethingVerbosely(foo *Foo, verbosity int) { // Could combine the next two lines, // with some loss of readability. prev := foo.Option(pkg.Verbosity(verbosity)) defer foo.Option(prev) // ... do some stuff with foo under high verbosity. }

11.Functional options type GreeterClient interface { SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) } type CallOption interface { before(*callInfo) error after(*callInfo) } // EmptyCallOption does not alter the Call configuration. type EmptyCallOption struct{} // TimeoutCallOption timeout option. type TimeoutCallOption struct { grpc.EmptyCallOption Timeout time.Duration }

12.Hybrid APIs // Dial connects to the Redis server at the given network and // address using the specified options. func Dial(network, address string, options ...DialOption) (Conn, error) // NewConn new a redis conn. func NewConn(c *Config) (cn Conn, err error) “JSON/YAML 配置怎么加载,无法映射 DialOption 啊!” “嗯,不依赖配置的走 options,配置加载走config”

13.Configuration & APIs “For example, both your infrastructure and interface might use plain JSON. However, avoid tight coupling between the data format you use as the interface and the data format you use internally. For example, you may use a data structure internally that contains the data structure consumed from configuration. The internal data structure might also contain completely implementation-specific data that never needs to be surfaced outside of the system.” -- the-site-reliability-workbook 2

14.Configuration & APIs // Dial connects to the Redis server at the given network and // address using the specified options. func Dial(network, address string, options ...DialOption) (Conn, error) • 仅保留 options API; • config file 和 options struct 解耦; 配置工具的实践: • 语义验证 • 高亮 • Lint • 格式化 YAML + Protobuf

15.Configuration & APIs package redis // Options apply config to options. func (c *Config) Options() []redis.Options { // Option configures how we set return []redis.Options{ up the connection. redis.DialDatabase(c.Database), type Option interface { redis.DialPassword(c.Password), apply(*options) redis.DialReadTimeout(c.ReadTimeout), } } } func main() { // instead use load yaml file. c := &Config{ Network: "tcp", Addr: "127.0.0.1:3389", Database: 1, Password: "Hello", ReadTimeout: 1 * time.Second, } r, _ := redis.Dial(c.Network, c.Addr, c.Options()...) }

16.Configuration & APIs func ApplyYAML(s *redis.Config, yml string) syntax = "proto3"; error { js, err := yaml.YAMLToJSON([]byte(yml)) import "google/protobuf/duration.proto"; if err != nil { return err } package config.redis.v1; return ApplyJSON(s, string(js)) } // redis config. message redis { // Options apply config to options. string network = 1; func Options(c *redis.Config) []redis.Options { string address = 2; return []redis.Options{ int32 database = 3; redis.DialDatabase(c.Database), string password = 4; redis.DialPassword(c.Password), redis.DialReadTimeout(c.ReadTimeout), google.protobuf.Duration read_timeout } = 5; } }

17.Configuration & APIs func main() { // load config file from yaml. c := new(redis.Config) _ = ApplyYAML(c, loadConfig()) r, _ := redis.Dial(c.Network, c.Address, Options(c)...) }

18.Configuration Best Pratice 代码更改系统功能是一个冗长且复杂的过程, 往往还涉及Review、测试等流程,但更改单 个配置选项可能会对功能产生重大影响,通常 配置还未经测试。 配置的目标: • 避免复杂 • 多样的配置 • 简单化努力 • 以基础设施 -> 面向用户进行转变 • 配置的必选项和可选项 • 配置的防御编程 • 权限和变更跟踪 • 配置的版本和应用对齐 • 安全的配置变更:逐步部署、回滚更改、自动回滚

19.Thanks