优点

服务可以做到动态更新,配置更新时不需要停服,且文件更新后不需要重启服务。

源码分析

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

// for alternative data
Context context.Context
}
type Option func(o *Options)

可以看到再Options里放了一个编码器和一个上下文管理器,并定义了一个Option的hook进写入了数据:

1
2
3
4
5
6
7
8
9
// WithPath sets the path to file
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
}

可以看到这里做了多次配置文件保底,分别是:

  1. 命令行获取代码配置地址,第一次保底,获取到配置文件地址后使用WithPath的hook写如了配置文件位置。本质上来说已经够了
1
StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file")
  1. 从上下文中判断有没有放入配置文件地址,如果没有的话拿代码里的配置.猜测使用该处代码的主要原因可能是为了防止上下文放入失败
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
// Options 配置的参数
type Options struct {
Loader loader.Loader
Reader reader.Reader
// 配置源
Source []source.Source

// for alternative data
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)
}
}

// WithEntity sets the config Entity
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
// NewConfig returns new config
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)
}
// 这里主要是补充程序启动时数据还未预置,自动更新流程不能触发,自动更新时在携程中运行的,流程答题相似
// 读文件 -> Loader -> 设置快照 -> 手动解析
// 携程watcher | |-> 自动解析
// default loader uses the configured reader
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 processing
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 {
// fallback
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) {
// watches a source for changes
watch := func(idx int, s source.Watcher) error {
for {
// get changeset
cs, err := s.Next()
if err != nil {
return err
}

m.Lock()

// save
m.sets[idx] = cs

// merge sets
set, err := m.opts.Reader.Merge(m.sets...)
if err != nil {
m.Unlock()
return err
}

// set values
m.vals, _ = m.opts.Reader.Values(set)
m.snap = &loader.Snapshot{
ChangeSet: set,
Version: genVer(),
}
m.Unlock()

// send watch updates
m.update()
}
}

for {
// watch the source
w, err := s.Watch()
if err != nil {
time.Sleep(time.Second)
continue
}

done := make(chan bool)

// the stop watch func
go func() {
select {
case <-done:
case <-m.exit:
}
_ = w.Stop()
}()

// block watch
if err := watch(idx, w); err != nil {
// do something better
time.Sleep(time.Second)
}

// close done chan
close(done)

// if the config is closed exit
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的源码,这里只做了少量魔改,基本流程都一样.