go工程项目配置管理

许兴文
2023-12-01

Configuration

  • 环境变量(配置)
    Region、Zone、Cluster、Environment、Color、Discovery、AppID、Host,等之类的环境信息,都是通过在线运行时平台打入到容器或者物理机,供 kit 库读取使用。
  • 静态配置
    资源需要初始化的配置信息,比如 http/gRPC server、redis、mysql 等,这类资源在线变更配置的风险非常大,我通常不鼓励 on-the-fly 变更,很可能会导致业务出现不可预期的事故,变更静态配置和发布 bianry app 没有区别,应该走一次迭代发布的流程。
  • 动态配置
    应用程序可能需要一些在线的开关,来控制业务的一些简单策略,会频繁的调整和使用,我们把这类是基础类型(int, bool)等配置,用于可以动态变更业务流的收归一起,同时可以考虑结合类似 https://pkg.go.dev/expvar 来结合使用。
  • 全局配置
    通常,我们依赖的各类组件、中间件都有大量的默认配置或者指定配置,在各个项目里大量拷贝复制,容易出现意外,所以我们使用全局配置模板来定制化常用的组件,然后再特化的应用里进行局部替换。

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 时间?”

要满足这些功能:添加功能 Add 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...

注意:一个 package 的公共方法越多,则意味着这个 package 越脆弱。

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())
}

Configuration struct API

从配置文件解析拿到一个对象,然后对redis做一个初始化

// 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" // 副作用是什么? 不清楚修改会导致什么结果出现
}

// NewConn new a redis conn. 做了一个deep copy传进去的,外面的人是无法修改内部值的。但是其无法区分零值和未设定
func NewConn(c Config) (cn Conn, err error)

// NewConn new a redis conn. 必须要传递参数,但是可以传递nil作为默认配置参数
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) // 这样使用默认配置,但通常不建议传递一个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

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)		// 由于这里的dialOptions是小写,所以别人调用是无法修改其option内部的,只能通过新增DialXxx方法才能修改option
}

// 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,
  }
    // 当option为nil的时候,也就是没有传参进来表示使用默认值,那么就不会去for循环中替换掉option.f中的do值,那么使用的就是默认值
  for _, option := range options {
    option.f(&do)
  } // ...
}

这时候demo就会修改为这样:

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))
}

在某些场景下需要把修改之后的配置还原:

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.
}

gRPC demo:

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
}

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”

// 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
  • 格式化

如下:

func ApplyYAML(s *redis.Config, yml string) error {	// 传进来的是yaml格式的config
  js, err := yaml.YAMLToJSON([]byte(yml))			// 先转换成json格式
  if err != nil {
    return err
  }
  return ApplyJSON(s, string(js))					// json格式再转换成protobuf
}
// Options apply config to options.
func Options(c *redis.Config) []redis.Options {	// redis.Options是redis client这个API库需要的配置参数信息 
  return []redis.Options{
    redis.DialDatabase(c.Database),
    redis.DialPassword(c.Password),
    redis.DialReadTimeout(c.ReadTimeout),
  }
}

最终代码:

func main() {
  // load config file from yaml.
  c := new(redis.Config)
  _ = ApplyYAML(c, loadConfig())
  r, _ := redis.Dial(c.Network, c.Address, Options(c)...)
}
 类似资料: