這個代碼庫主要用於加載和解析配置文件,支持 JSON、TOML 和 YAML 格式。主要功能包括從文件或字節數據中加載配置、填充默認值以及處理配置數據的鍵大小寫。代碼的主要結構和函數如下:
- fieldInfo 結構體:用於表示字段信息,包括子字段和映射字段。
- 從文件或字節數據加載配置的函數:Load, LoadConfig, LoadFromJsonBytes, LoadConfigFromJsonBytes, LoadFromTomlBytes, LoadFromYamlBytes, LoadConfigFromYamlBytes 和 MustLoad。
- 構建和處理字段信息的函數:buildFieldsInfo, buildNamedFieldInfo, buildAnonymousFieldInfo, buildStructFieldsInfo, addOrMergeFields 和 mergeFields。
- 處理字符串、映射和數組數據的輔助函數: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函數開始功能實現
- Load 函數用於根據指定的配置文件路徑加載配置數據。它首先根據文件擴展名調用 loaders 中相應的加載函數讀取文件內容,然後調用 Unmarshal 方法解析文件內容並將其映射到提供的結構體實例中。
- loaders 變量定義了一個映射,它包含了不同文件擴展名(如 .json, .toml, .yaml, .yml)和相應的加載函數。這些加載函數負責從不同格式的配置文件中讀取數據並解析為字節序列。
- FillDefault 函數用於為提供的結構體實例填充默認值。它使用全局變量 fillDefaultUnmarshaler 調用 Unmarshal 方法來實現這一功能。
- 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),執行不同的操作:
- 如果 i 是一個 map[string]any 類型,函數首先檢查 elemType 是否為結構體類型。如果不是,返回 errTypeMismatch 錯誤。否則,使用 mapValuer 函數將 i 轉換為 mapValuer 類型,然後調用 UnmarshalValuer 方法。
- 如果 i 是一個 []any 類型,函數首先檢查 elemType 是否為切片類型。如果不是,返回 errTypeMismatch 錯誤。否則,調用 fillSlice 方法將 i 的內容填充到 v 對應的切片中。
- 如果 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 < refValue.Len(); i++ {
ithValue := refValue.Index(i).Interface()
if ithValue == nil {
continue
}
valid = true
switch dereffedBaseKind {
case reflect.Struct:
target := reflect.New(dereffedBaseType)
if err := u.Unmarshal(ithValue.(map[string]any), target.Interface()); err != nil {
return err
}
SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
case reflect.Slice:
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue); err != nil {
return err
}
default:
if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue); err != nil {
return err
}
}
}
if valid {
value.Set(conv)
}
return nil
}
fillSlice方法用於將解析後的配置數據填充到一個切片(slice)類型的字段中。
該方法執行以下操作:
- 首先,檢查 value 是否可設置(可賦值),如果不可設置則返回錯誤。
- 獲取切片的元素類型 baseType,同時獲取去除指針層級後的基本類型 dereffedBaseType 和基本類型的 Kind。
- 根據輸入的 mapValue 創建一個新的切片 conv。
- 如果輸入切片的長度為 0,則設置 value 為新創建的空切片並返回。
-
遍歷輸入的 mapValue,針對每個元素,根據 dereffedBaseKind 的類型執行相應的操作:
如果是結構體類型,使用 u.Unmarshal() 方法將數據解析到一個新的結構體實例中,然後設置新實例的值到目標切片的對應位置。 如果是切片類型,遞歸調用 fillSlice() 方法處理嵌套的切片。 其他情況下,使用 fillSliceValue() 方法填充切片元素的值。 如果存在有效元素,將目標切片 conv 設置為 value。 - 返回 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 < numFields; i++ {
field := baseType.Field(i)
if !field.IsExported() {
continue
}
if err := u.processField(field, valElem.Field(i), m, fullName); err != nil {
return err
}
}
return nil
}
該方法首先調用 unmarshalWithFullName() 方法,傳入一個 valuerWithParent 類型的參數 simpleValuer{current: m},目標實例 v 和一個空字符串表示全名。
unmarshalWithFullName() 方法執行以下操作:
- 驗證輸入的 v 是否為一個有效的指針,如果不是則返回錯誤。
- 獲取輸入參數的類型和去除指針層級後的基本類型。如果基本類型不是結構體,返回錯誤。
- 獲取 v 的反射值的元素 valElem。如果元素是指針類型,創建一個新的結構體實例並設置為 valElem。
-
遍歷結構體的每個字段:
如果字段不是導出的(非公開),則跳過。 調用 processField() 方法處理每個字段,將解析的值設置到目標結構體實例的對應字段中。如果出現錯誤,返回錯誤。 - 返回 nil,表示成功執行。