深入理解Hugo - Config模块applyConfigDefaults之源码实现

109 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

了然于胸 - applyConfigDefaults时序图

上面的loadConfig帮助我们读取了文件系统里的config.toml文件,并将结果以map[string]any的格式。 那我们会把用户自定义的这些配置信息最终存储到哪儿,又将以什么形式提供服务呢?

我们来通过applyConfigDefaults时序图找寻一下线索: 8.2-LoadConfig-sequence flow-applyConfigDefaults.svg

从上图左下脚可以看到在configLoader调用applyConfigDefaults方法后,实际调用的是l.cfg.SetDefaults

// hugo-playground/hugolib/config.go
// line 54
func (l configLoader) applyConfigDefaults() error {
   defaultSettings := maps.Params{
      ...
      "timeout":                              "30s",
      ...
   }

   l.cfg.SetDefaults(defaultSettings)

   return nil
}

l.cfg从哪儿来呢? 我们可以往时序图左上方看。 在创建configLoader实例的时候,有一同创建cfg,直接调用config.New()

// hugo-playground/config/defaultConfigProvider.go
// line 16
// New creates a Provider backed by an empty maps.Params.
func New() Provider {
   return &defaultConfigProvider{
      root: make(maps.Params),
   }
}

实际返回的是一个接口Provider,而实现了这个接口的对象就是defaultConfigProvider,一起初始化的还有字段maps.Params类型的root

看来Provider就是默认配置项的接收者。 为了进一步验证我们的猜测,我们继续往右边看。 果然,在从config.toml中读到取用户的自定义配置信息后,也是调用的Provider(cfg)Set方法,将解析后的map[string]any值设置到了""字段。

回到我们最初的问题:我们会把用户自定义的这些配置信息最终存储到哪儿,又将以什么形式提供服务呢?

这时我们已经知道,不管是用户自定义信息,还是默认配置信息,我们都存储在了这个配置提供者里面。 相信ConfigProvider就是我们要找寻的答案。 是时候来看看这个Provider接口的定义了:

// hugo-playground/config/configProvider.go
// line 9
// Provider provides the configuration settings for Hugo.
type Provider interface {
   ...
   Get(key string) any
   Set(key string, value any)
   SetDefaults(params maps.Params)
   ...
}

确实,Provider不仅提供了Set方法,还提供了Get方法,那就没跑了。

弄清了起点和终点后,我们继续看看过程,SetSetDefaults方法具体是怎么将不同类型的数据设置到Provider中的。

先看负责用户自定义配置项的Set方法。 拿到用户的配置信息后,首先需要将配置信息转换成Params类型:

// hugo-playground/common/maps/params.go
// line 9
// Params is a map where all keys are lower case.
type Params map[string]any

实际他俩类型是一样的,都是map[string]any

在类型转换成Params后,需要通过PrepareParams进行处理,让内部格式保持统一。 而Params所做的主要事情包括两项:一是将所有key都转换成小写字符串,二是将所有的值都转换成通用类型any。 这样的好处就是信息都按统一标准Params格式进行存储,因为类型确定,所以方便拓展和提供其它类型的数据服务。 比如想要获取整形数据,可以在Provider接口中定义GetInt方法,那我们就可以将所获取的any类型的值,转换成Int。 同理,如果需要其它类型的数据服务,一样可以满足。

再看负责默认配置的SetDefaults方法,比Set方法更简单,直接调用的就是PrepareParams。 因为自己定义的格式,自己最清楚如何使用。 相对面向用户的配置信息多样性,自己定义的默认值更具备确定性。

抽象总结 - 输入不同类型的值,输出标准的configProvider

8.3-LoadConfig-config.svg

从输入来看,我们要接收来自用户的自定义配置信息,同时也要接收默认的自定义配置信息,还有以后可能会碰到的单项更新信息。

从输出来看,我们需要提供一个通用的配置信息提供方,统一标准,方便满足获取不同数据类型的需求。

为了正确处理接收到的数据,并满足多样性数据服务的需求,Hugo将所有接收到的数据进行标准化处理,以统一的Params格式式进行存储。 这样就可以在标准化的基础上进行拓展,从而满足不同的数据需求。

动手实践 - Show Me the Code of applyConfigDefaults

在知道applyConfigDefaults的实现原理后,我们再来动动小手,用代码来总结代码,巩固一下知识。

可以这里线上尝试,Show Me the Code, try it yourself

代码里有注解说明,代码样例:

package main

import (
   "fmt"
   "reflect"
   "strings"
)

// Provider 定义提供方需要具备的能力
// 通过Key查询值
// 设置键值对
// 设置默认参数
type Provider interface {
   Get(key string) any
   Set(key string, value any)
   SetDefaults(params Params)
}

// Params 参数格式定义
// 关键字为字符类型
// 值为通用类型any
type Params map[string]any

// Set 根据新传入参数,对应层级进行重写
// pp为新传入参数
// p为当前参数
// 将pp的值按层级结构写入p
// 递归完成
func (p Params) Set(pp Params) {
   for k, v := range pp {
      vv, found := p[k]
      if !found {
         p[k] = v
      } else {
         switch vvv := vv.(type) {
         case Params:
            if pv, ok := v.(Params); ok {
               vvv.Set(pv)
            } else {
               p[k] = v
            }
         default:
            p[k] = v
         }
      }
   }
}

func New() Provider {
   return &defaultConfigProvider{
      root: make(Params),
   }
}

// defaultConfigProvider Provider接口实现对象
type defaultConfigProvider struct {
   root Params
}

// Get 按key获取值
// 约定""键对应的是c.root
// 嵌套获取值
func (c *defaultConfigProvider) Get(k string) any {
   if k == "" {
      return c.root
   }
   key, m := c.getNestedKeyAndMap(strings.ToLower(k))
   if m == nil {
      return nil
   }
   v := m[key]
   return v
}

// getNestedKeyAndMap 支持多级查询
// 通过分隔符"."获取查询路径
func (c *defaultConfigProvider) getNestedKeyAndMap(
   key string) (string, Params) {
   var parts []string
   parts = strings.Split(key, ".")
   current := c.root
   for i := 0; i < len(parts)-1; i++ {
      next, found := current[parts[i]]
      if !found {
         return "", nil
      }
      var ok bool
      current, ok = next.(Params)
      if !ok {
         return "", nil
      }
   }
   return parts[len(parts)-1], current
}

// Set 设置键值对
// 统一key的格式为小写字母
// 如果传入的值符合Params的要求,通过root进行设置
// 如果为非Params类型,则直接赋值
func (c *defaultConfigProvider) Set(k string, v any) {
   k = strings.ToLower(k)

   if p, ok := ToParamsAndPrepare(v); ok {
      // Set the values directly in root.
      c.root.Set(p)
   } else {
      c.root[k] = v
   }

   return
}

// SetDefaults will set values from params if not already set.
func (c *defaultConfigProvider) SetDefaults(
   params Params) {
   PrepareParams(params)
   for k, v := range params {
      if _, found := c.root[k]; !found {
         c.root[k] = v
      }
   }
}

// ToParamsAndPrepare converts in to Params and prepares it for use.
// If in is nil, an empty map is returned.
// See PrepareParams.
func ToParamsAndPrepare(in any) (Params, bool) {
   if IsNil(in) {
      return Params{}, true
   }
   m, err := ToStringMapE(in)
   if err != nil {
      return nil, false
   }
   PrepareParams(m)
   return m, true
}

// IsNil reports whether v is nil.
func IsNil(v any) bool {
   if v == nil {
      return true
   }

   value := reflect.ValueOf(v)
   switch value.Kind() {
   case reflect.Chan, reflect.Func,
      reflect.Interface, reflect.Map,
      reflect.Ptr, reflect.Slice:
      return value.IsNil()
   }

   return false
}

// ToStringMapE converts in to map[string]interface{}.
func ToStringMapE(in any) (map[string]any, error) {
   switch vv := in.(type) {
   case Params:
      return vv, nil
   case map[string]string:
      var m = map[string]any{}
      for k, v := range vv {
         m[k] = v
      }
      return m, nil

   default:
      fmt.Println("value type not supported yet")
      return nil, nil
   }
}

// PrepareParams
// * makes all the keys lower cased
// * This will modify the map given.
// * Any nested map[string]interface{}, map[string]string
// * will be converted to Params.
func PrepareParams(m Params) {
   for k, v := range m {
      var retyped bool
      lKey := strings.ToLower(k)

      switch vv := v.(type) {
      case map[string]any:
         var p Params = v.(map[string]any)
         v = p
         PrepareParams(p)
         retyped = true
      case map[string]string:
         p := make(Params)
         for k, v := range vv {
            p[k] = v
         }
         v = p
         PrepareParams(p)
         retyped = true
      }

      if retyped || k != lKey {
         delete(m, k)
         m[lKey] = v
      }
   }
}

func main() {
   // 新建Config Provider实例
   // 实例中defaultConfigProvider实现了接口
   provider := New()

   // 模拟设置用户自定义配置项
   // config.toml中关于主题的配置信息
   // 类型是map[string]string
   // 需要转换成map[string]any,也就是Params类型
   provider.Set("", map[string]string{
      "theme": "mytheme",
   })

   // 模拟默认配置项
   // 超时默认时间为30秒
   provider.SetDefaults(Params{
      "timeout": "30s",
   })

   // 输出提前设置的所有配置信息
   fmt.Printf("%#v\n", provider.Get(""))
   fmt.Printf("%#v\n", provider.Get("theme"))
   fmt.Printf("%#v\n", provider.Get("timeout"))
}

程序输出结果:

# 输出Config Provider提前设置好的信息
# 包括用户自定义信息,和默认信息
# 准备Input:自定义信息,默认信息
# 得到Output: 通过Config Provider获取所有配置信息
main.Params{"theme":"mytheme", "timeout":"30s"}
"mytheme"
"30s"

Program exited.