源码阅读:go-zero的core/conf包

2023年3月29日

这个代码库主要用于加载和解析配置文件,支持 JSON、TOML 和 YAML 格式。主要功能包括从文件或字节数据中加载配置、填充默认值以及处理配置数据的键大小写。代码的主要结构和函数如下:

  1. fieldInfo 结构体:用于表示字段信息,包括子字段和映射字段。
  2. 从文件或字节数据加载配置的函数:Load, LoadConfig, LoadFromJsonBytes, LoadConfigFromJsonBytes, LoadFromTomlBytes, LoadFromYamlBytes, LoadConfigFromYamlBytes 和 MustLoad。
  3. 构建和处理字段信息的函数:buildFieldsInfo, buildNamedFieldInfo, buildAnonymousFieldInfo, buildStructFieldsInfo, addOrMergeFields 和 mergeFields。
  4. 处理字符串、映射和数组数据的辅助函数:toLowerCase, toLowerCaseInterface, toLowerCaseKeyMap,以及表示键重复错误的自定义类型 dupKeyError 和相关函数。

整个库的功能是通过反射和递归地处理结构体字段信息来实现的。在加载配置时,首先将 TOML 和 YAML 格式的数据转换为 JSON 格式,然后统一处理 JSON 数据。配置数据加载后,库会确保数据的键与结构体字段的名称匹配,以便将数据正确地填充到结构体中。

开始

起因是在阅读快速开始go-zero服务时,主函数调用了这两个包,为了方便理解主函数和go-zero框架,同时也为了学习优质源码,提高代码能力,快速阅读了这两个包的内容。
go-zero-demo
https://go-zero.dev/cn/docs/quick-start/monolithic-service
其中主函数如下

package main

import (
    "flag"
    "fmt"

    "go-zero-demo/greet/internal/config"
    "go-zero-demo/greet/internal/handler"
    "go-zero-demo/greet/internal/svc"

    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/rest"
)

var configFile = flag.String("f", "etc/greet-api.yaml", "the config file")

func main() {
    flag.Parse()

    var c config.Config
    conf.MustLoad(*configFile, &c)

    ctx := svc.NewServiceContext(c)
    server := rest.MustNewServer(c.RestConf)
    defer server.Stop()

    handler.RegisterHandlers(server, ctx)

    fmt.Printf("Starting server at %s:%d...n", c.Host, c.Port)
    server.Start()
}

其中

在主函数中,我们见到了这个语句

var configFile = flag.String("f", "etc/greet-api.yaml", "the config file")

    var c config.Config
    conf.MustLoad(*configFile, &c)

它调用了core/conf包对外的接口,对默认的配置文件进行了分析

core/conf包

调用

// MustLoad loads config into v from path, exits on error.
func MustLoad(path string, v any, opts ...Option) {
    if err := Load(path, v, opts...); err != nil {
        log.Fatalf("error: config file %s, %s", path, err.Error())
    }
}

执行了Load,接下来由Load函数开始功能实现

  1. Load 函数用于根据指定的配置文件路径加载配置数据。它首先根据文件扩展名调用 loaders 中相应的加载函数读取文件内容,然后调用 Unmarshal 方法解析文件内容并将其映射到提供的结构体实例中。
  2. loaders 变量定义了一个映射,它包含了不同文件扩展名(如 .json, .toml, .yaml, .yml)和相应的加载函数。这些加载函数负责从不同格式的配置文件中读取数据并解析为字节序列。
  3. FillDefault 函数用于为提供的结构体实例填充默认值。它使用全局变量 fillDefaultUnmarshaler 调用 Unmarshal 方法来实现这一功能。
  4. Unmarshaler 结构体及其相关方法。Unmarshaler 结构体负责解析从配置文件中读取到的数据,将其解析为对应的 Go 结构体。Unmarshal 方法是实现这一功能的关键,它根据输入数据的类型(如 map、slice 等)调用相应的解析函数。
var (
    fillDefaultUnmarshaler = mapping.NewUnmarshaler(jsonTagKey, mapping.WithDefault())
    loaders                = map[string]func([]byte, any) error{
        ".json": LoadFromJsonBytes,
        ".toml": LoadFromTomlBytes,
        ".yaml": LoadFromYamlBytes,
        ".yml":  LoadFromYamlBytes,
    }
)

// children and mapField should not be both filled.
// named fields and map cannot be bound to the same field name.
type fieldInfo struct {
    children map[string]*fieldInfo
    mapField *fieldInfo
}

// FillDefault fills the default values for the given v,
// and the premise is that the value of v must be guaranteed to be empty.
func FillDefault(v any) error {
    return fillDefaultUnmarshaler.Unmarshal(map[string]any{}, v)
}

// Load loads config into v from file, .json, .yaml and .yml are acceptable.
func Load(file string, v any, opts ...Option) error {
    content, err := os.ReadFile(file)
    if err != nil {
        return err
    }

    loader, ok := loaders[strings.ToLower(path.Ext(file))]
    if !ok {
        return fmt.Errorf("unrecognized file type: %s", file)
    }

    var opt options
    for _, o := range opts {
        o(&opt)
    }

    if opt.env {
        return loader([]byte(os.ExpandEnv(string(content))), v)
    }

    return loader(content, v)
}

负责解析的函数Unmarshal

其中负责解析的函数Unmarshal位于core/mapping/unmarshaler.go中,由于这个库文件有一千多行,在此不过多了解,只了解这个主要的函数的实现
详细代码如下

// Unmarshal unmarshals m into v.
func (u *Unmarshaler) Unmarshal(i any, v any) error {
    valueType := reflect.TypeOf(v)
    if valueType.Kind() != reflect.Ptr {
        return errValueNotSettable
    }

    elemType := Deref(valueType)
    switch iv := i.(type) {
    case map[string]any:
        if elemType.Kind() != reflect.Struct {
            return errTypeMismatch
        }

        return u.UnmarshalValuer(mapValuer(iv), v)
    case []any:
        if elemType.Kind() != reflect.Slice {
            return errTypeMismatch
        }

        return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv)
    default:
        return errUnsupportedType
    }
}

它将参数 i(一个 any 类型,即任意类型)解析并赋值到参数 v(也是一个 any 类型)。此方法主要用于解析配置文件并将其内容填充到给定的结构体中。

函数首先检查 v 是否为指针类型,因为只有指针类型才能进行赋值。接下来,根据 i 的类型(map[string]any 或 []any),执行不同的操作:

  1. 如果 i 是一个 map[string]any 类型,函数首先检查 elemType 是否为结构体类型。如果不是,返回 errTypeMismatch 错误。否则,使用 mapValuer 函数将 i 转换为 mapValuer 类型,然后调用 UnmarshalValuer 方法。
  2. 如果 i 是一个 []any 类型,函数首先检查 elemType 是否为切片类型。如果不是,返回 errTypeMismatch 错误。否则,调用 fillSlice 方法将 i 的内容填充到 v 对应的切片中。
  3. 如果 i 既不是 map[string]any 类型,也不是 []any 类型,函数返回 errUnsupportedType 错误,表示不支持的类型。

通过 Unmarshal 方法,可以方便地将配置文件内容解析并填充到指定的结构体中。

对于两种情况,分别调用了两个方法

fillSlice方法

func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue any) error {
    if !value.CanSet() {
        return errValueNotSettable
    }

    baseType := fieldType.Elem()
    dereffedBaseType := Deref(baseType)
    dereffedBaseKind := dereffedBaseType.Kind()
    refValue := reflect.ValueOf(mapValue)
    if refValue.IsNil() {
        return nil
    }

    conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
    if refValue.Len() == 0 {
        value.Set(conv)
        return nil
    }

    var valid bool
    for i := 0; i 

fillSlice方法用于将解析后的配置数据填充到一个切片(slice)类型的字段中。
该方法执行以下操作:

  1. 首先,检查 value 是否可设置(可赋值),如果不可设置则返回错误。
  2. 获取切片的元素类型 baseType,同时获取去除指针层级后的基本类型 dereffedBaseType 和基本类型的 Kind。
  3. 根据输入的 mapValue 创建一个新的切片 conv。
  4. 如果输入切片的长度为 0,则设置 value 为新创建的空切片并返回。
  5. 遍历输入的 mapValue,针对每个元素,根据 dereffedBaseKind 的类型执行相应的操作:

     如果是结构体类型,使用 u.Unmarshal() 方法将数据解析到一个新的结构体实例中,然后设置新实例的值到目标切片的对应位置。
     如果是切片类型,递归调用 fillSlice() 方法处理嵌套的切片。
     其他情况下,使用 fillSliceValue() 方法填充切片元素的值。
     如果存在有效元素,将目标切片 conv 设置为 value。
  6. 返回 nil,表示成功执行。

UnmarshalValuer方法

,它接受一个 Valuer 类型的参数 m,将解析后的配置数据填充到目标结构体实例 v 中,提供了从数据源获取值的方法。

// UnmarshalValuer unmarshals m into v.
func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error {
    return u.unmarshalWithFullName(simpleValuer{current: m}, v, "")
}

func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error {
    rv := reflect.ValueOf(v)
    if err := ValidatePtr(&rv); err != nil {
        return err
    }

    valueType := reflect.TypeOf(v)
    baseType := Deref(valueType)
    if baseType.Kind() != reflect.Struct {
        return errValueNotStruct
    }

    valElem := rv.Elem()
    if valElem.Kind() == reflect.Ptr {
        target := reflect.New(baseType).Elem()
        SetValue(valueType.Elem(), valElem, target)
        valElem = target
    }

    numFields := baseType.NumField()
    for i := 0; i 

该方法首先调用 unmarshalWithFullName() 方法,传入一个 valuerWithParent 类型的参数 simpleValuer{current: m},目标实例 v 和一个空字符串表示全名。

unmarshalWithFullName() 方法执行以下操作:

  1. 验证输入的 v 是否为一个有效的指针,如果不是则返回错误。
  2. 获取输入参数的类型和去除指针层级后的基本类型。如果基本类型不是结构体,返回错误。
  3. 获取 v 的反射值的元素 valElem。如果元素是指针类型,创建一个新的结构体实例并设置为 valElem。
  4. 遍历结构体的每个字段:

     如果字段不是导出的(非公开),则跳过。
     调用 processField() 方法处理每个字段,将解析的值设置到目标结构体实例的对应字段中。如果出现错误,返回错误。
  5. 返回 nil,表示成功执行。

服务器托管,北京服务器托管,服务器租用 http://www.hhisp.net

hackdl

咨询热线/微信 13051898268