优点
服务可以做到动态更新,配置更新时不需要停服,且文件更新后不需要重启服务。
源码分析
1 2 3 4 5
| config.Setup( file.NewSource(file.WithPath(configYml)), database.Setup, storage.Setup, )
|
Options思想
在python中,形参可以定义为可选参数,但是在go这种强语法的语言中形参总是不可变的,所以我们就要引入一种Options的方式来将不可变的参数转换为可变的参数,如下边一段代码:
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
| package main
import "fmt"
type Options struct { Test1 string Test2 string }
type Option func(*Options)
func WithTest1(t1 string) Option { return func(o *Options) { o.Test1 = t1 } }
func NewOptions(opts ...Option) Options { options := Options{} for _, opt := range opts { opt(&options) } return options }
func main() { opts := NewOptions(WithTest1("test1")) fmt.Println(opts) }
|
Options相当于作为了新建结构体函数的形参列表,有点类似于ts中的interface机制。但是在python中,可变的形参可以直接用等号连接,并且在调用的时候可以直接进行赋值,但是在go中需要引入Option和With函数。
With函数本质上就指Option,在创建对象时用户可以先调用With函数生成可变参数操作的引用,然后再创建函数中进行遍历,达到了可变的结构体初始化函数形参。
源文件封装
程序在此处开始了配置文件解析,引入了文件对象的Options:
1 2 3 4 5 6 7 8
| type Options struct { Encoder encoder.Encoder
Context context.Context } type Option func(o *Options)
|
可以看到再Options里放了一个编码器和一个上下文管理器,并定义了一个Option的hook进写入了数据:
1 2 3 4 5 6 7 8 9
| func WithPath(p string) source.Option { return func(o *source.Options) { if o.Context == nil { o.Context = context.Background() } o.Context = context.WithValue(o.Context, filePathKey{}, p) } }
|
这里的逻辑是将文件路径存放到上下文管理器中,而后,NewSource生成了一个file管理器放了Options和filePath:
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
| type file struct { path string opts source.Options } func NewSource(opts ...source.Option) source.Source { options := source.NewOptions(opts...) path := DefaultPath f, ok := options.Context.Value(filePathKey{}).(string) if ok { path = f } return &file{opts: options, path: path} } func NewOptions(opts ...Option) Options { options := Options{ Encoder: json.NewEncoder(), Context: context.Background(), } for _, o := range opts { o(&options) }
return options }
|
可以看到这里做了多次配置文件保底,分别是:
- 命令行获取代码配置地址,第一次保底,获取到配置文件地址后使用WithPath的hook写如了配置文件位置。本质上来说已经够了
1
| StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file")
|
- 从上下文中判断有没有放入配置文件地址,如果没有的话拿代码里的配置.猜测使用该处代码的主要原因可能是为了防止上下文放入失败
1 2 3 4
| f, ok := options.Context.Value(filePathKey{}).(string) if ok { path = f }
|
配置封装
至此,我们已经拿到了编码器和配置文件的位置,我们可以开始做配置解析了:
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
| type Options struct { Loader loader.Loader Reader reader.Reader Source []source.Source
Context context.Context
Entity Entity } func Setup(s source.Source, fs ...func()) { _cfg = &Settings{ Settings: Config{ ... }, callbacks: fs, } var err error config.DefaultConfig, err = config.NewConfig( config.WithSource(s), config.WithEntity(_cfg), ) if err != nil { log.Fatal(fmt.Sprintf("New config object fail: %s", err.Error())) } _cfg.Init() }
|
首先这里边定义了一个新的Options,可以认为是配置选项器.代码里引入了新Option:
1 2 3 4 5 6 7 8 9 10 11 12
| func WithSource(s source.Source) Option { return func(o *Options) { o.Source = append(o.Source, s) } }
func WithEntity(e Entity) Option { return func(o *Options) { o.Entity = e } }
|
先把源目放入管理器,然后管理器进行解析:
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
| func NewConfig(opts ...Option) (Config, error) { return newConfig(opts...) } func newConfig(opts ...Option) (Config, error) { var c config
err := c.Init(opts...) if err != nil { return nil, err } go c.run()
return &c, nil } func (c *config) Init(opts ...Option) error { c.opts = Options{ Reader: json.NewReader(), } c.exit = make(chan bool) for _, o := range opts { o(&c.opts) } if c.opts.Loader == nil { c.opts.Loader = memory.NewLoader(memory.WithReader(c.opts.Reader)) }
err := c.opts.Loader.Load(c.opts.Source...) if err != nil { return err }
c.snap, err = c.opts.Loader.Snapshot() if err != nil { return err }
c.vals, err = c.opts.Reader.Values(c.snap.ChangeSet) if err != nil { return err } if c.opts.Entity != nil { _ = c.vals.Scan(c.opts.Entity) }
return nil }
|
先来关注这部分代码:
从这个调用链不难看出,代码一进来就调起了配置管理器的初始化.这里主要时对配置管理器进行了初始化操作,放入了Reader和Loader。
在初始化Loader的时候,创建了一个协程监听文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| for i, s := range options.Source { m.sets[i] = &source.ChangeSet{Source: s.String()} go m.watch(i, s) }
for _, source := range sources { set, err := source.Read() if err != nil { gerrors = append(gerrors, fmt.Sprintf("error loading source %s: %v", source, err)) continue } m.Lock() m.sources = append(m.sources, source) m.sets = append(m.sets, set) idx := len(m.sets) - 1 m.Unlock() go m.watch(idx, source) }
|
当监听到为文件变化时出发读取,文件属性以及文件值会被更新,并且创建一个快照。
而后回对所有文件全部注意转码:
1 2 3
| if err := m.reload(); err != nil { gerrors = append(gerrors, err.Error()) }
|
1 2 3 4 5
| codec, ok := j.opts.Encoding[m.Format] if !ok { codec = j.json }
|
转为json后便可直接用json解析器解析配置到结构体了:
1 2 3 4 5 6 7
| func (j *jsonValues) Scan(v interface{}) error { b, err := j.sj.MarshalJSON() if err != nil { return err } return json.Unmarshal(b, v) }
|
动态更新
上边启动过一个文件监听携程:
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 63 64 65 66 67 68 69 70 71
| func (m *memory) watch(idx int, s source.Source) { watch := func(idx int, s source.Watcher) error { for { cs, err := s.Next() if err != nil { return err }
m.Lock()
m.sets[idx] = cs
set, err := m.opts.Reader.Merge(m.sets...) if err != nil { m.Unlock() return err }
m.vals, _ = m.opts.Reader.Values(set) m.snap = &loader.Snapshot{ ChangeSet: set, Version: genVer(), } m.Unlock()
m.update() } }
for { w, err := s.Watch() if err != nil { time.Sleep(time.Second) continue }
done := make(chan bool)
go func() { select { case <-done: case <-m.exit: } _ = w.Stop() }()
if err := watch(idx, w); err != nil { time.Sleep(time.Second) }
close(done)
select { case <-m.exit: return default: } } }
|
当文件变化时,触发Next函数,Next将新读到的文件信息返回出来,把新文件信息放入sets后重新进入Merge进行转码,需要注意的是,这里的重转码并不是只针对单个配置文件,而是针对所有文件.sets更新后进入Values对配置信息进行解析
回顾一下上面的代码,一共启动了两个携程:
1 2 3
| func newConfig(opts ...Option) (Config, error) { go c.run() }
|
这个携程是进行配置解析以及回调的,也就是说现在存在两个携程,使用一个公共内存通信:
1
| watcher -> source -> config.run
|
结构体监听主要得益于前面在config中添加的snapshot,代码循环比较当前配置和快照是否相同,如果不相同则触发重解析并重新运行初始化回调.然后更新快照,继续等待下一次更新.
不得不说,这个设计很复杂,但是也很巧妙,梳理的比较粗糙,要想知道详细的工作流程可以看看go-micro/config的源码,这里只做了少量魔改,基本流程都一样.