如何 reflect 使用 json tag

193 阅读3分钟

在学习了 利用 reflect 给基本类型设置值 之后,我们再来看看如何在 reflect 中使用 json tag

默认值设置

env 这个库中内置 env 这个 json tag,去环境变量中去读 env 指定的 key

如果环境变量中没有这个 key 需要一个默认值,怎么办呢?

它提供了 envDefault 这个 json tag

type Config struct {
    StringWithDefault string   `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/db"`
}

通过 filed.Tag.Lookup("envDefault") 就可以读到 envDefault 这个 json tag 指定的内容了

field := refType.Field(i)
defaultValue, hasDefaultValue := field.Tag.Lookup(opts.DefaultValueTagName)

当然 Field.Tag.Get() 也可以

字符串切割

如果某个环境变量有多个值,比如 STRINGS=str1,str2 解析出来是个后应该要转成切片的形式:[str, str2]

env 这个库默认是用 "," 做分割的

如果想要自己指定分割符,它提供了一个 envSeparatorjson tag

type Config struct {
    CustomSeparator   []string `env:"SEPSTRINGS" envSeparator:":"`
}

通过 field.Tag.Get("envSeparator") 获取分隔符

separator := field.Tag.Get("envSeparator")
if separator == "" {
    separator = ","
}
parts := strings.Split("str1:str2", separator)

这样就可以自定义分割符了

字段小写开头,默认忽略

type Config struct {
    unexported string
    Exported   string
}

判断结构体中的字段是不是未导出的,有两种方法

  1. fieldType.CanSet(),如果这个字段不能不是设置,说明这个字段是未导出的字段
c := &Config{}

ptrRef := reflect.ValueOf(c)
ref := ptrRef.Elem()
refType := ref.Type()

for i := 0; i < ref.NumField(); i++ {
    field := refType.Field(i)
    fieldType := ref.Field(i)
    if !fieldType.CanSet() {
    fmt.Printf("私有字段: %s\n", field.Name)
    } else {
    fmt.Printf("公开字段: %s\n", field.Name)
    }
}
  1. field.PkgPath != "",如果 PkgPath 不能 "" 字符串,说明是未导出的字段
c := &Config{}

ptrRef := reflect.ValueOf(c)
ref := ptrRef.Elem()
refType := ref.Type()

for i := 0; i < ref.NumField(); i++ {
    field := refType.Field(i)
    if field.PkgPath != "" {
    fmt.Printf("私有字段: %s\n", field.Name)
    } else {
    fmt.Printf("公开字段: %s\n", field.Name)
    }
}

解析嵌套结构体

如果结构体中的字段是个结构体类型,该如何处理呢?

type Config struct {
    NonDefined struct {
        String string `env:"NONDEFINED_STR"`
    }
}

通过 Kind() 函数可以判断当前的字段的类型,如果是结构体的话,就递归调用 doParse() ,继续解析结构体

func doParseField(refField reflect.Value, refTypeField reflect.StructField, processField processFieldFn, opts Options) error {
    if refField.Kind() == reflect.Struct {
        return doParse(refField, processField, opts)
    }
}

添加字段前缀

如果是嵌套结构体,那么我们希望的是每层有自己的 key,最终的 key 应该是每一层的名字拼接

比如下面的结构体,我们希望的环境变量的 keyPRF_NONDEFINED_STR

type Config struct {
    NestedNonDefined struct {
        NonDefined struct {
        String string `env:"STR"`
        } `env:"NONDEFINED_"`
    } `env:"PRF_"`
}

env 这个库提供了 envPrefixjson tag

type Config struct {
    NestedNonDefined struct {
        NonDefined struct {
        String string `env:"STR"`
        } `envPrefix:"NONDEFINED_"`
    } `envPrefix:"PRF_"`
}

在上面解析嵌套结构体时,我们传给 doParse 是默认的 options,在这里就需要合并每一层的 envPrefix

func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options {
    return Options{
    Environment:         opts.Environment,
    TagName:             opts.TagName,
    PrefixTagName:       opts.PrefixTagName,
    Prefix:              opts.Prefix + field.Tag.Get(opts.PrefixTagName),
    DefaultValueTagName: opts.DefaultValueTagName,
    FuncMap:             opts.FuncMap,
    }
}

在解析结构体时调用 optionsWithEnvPrefix() 合并上一层的 options

func doParseField(refField reflect.Value, refTypeField reflect.StructField, processField processFieldFn, opts Options) error {
    if refField.Kind() == reflect.Struct {
        return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
    }
}

time.Location

time.Location 是用于表示特定时区的类型,它是一个结构体

type Config struct {
    Location     time.Location    `env:"LOCATION"`
    Locations    []time.Location  `env:"LOCATIONS"`
    LocationPtr  *time.Location   `env:"LOCATION"`
    LocationPtrs []*time.Location `env:"LOCATIONS"`
}

在解析 time.Location 时,refField.Kind() == reflect.Struct 这个判断应该放在 processField() 函数下面

func doParseField(refField reflect.Value, refTypeField reflect.StructField, processField processFieldFn, opts Options) error {
    if !refField.CanSet() {
    return nil
    }
    //if refField.Kind() == reflect.Struct {
    // return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
    //}
    params, err := parseFieldParams(refTypeField, opts)
    if err != nil {
    return err
    }

    if err := processField(refField, refTypeField, opts, params); err != nil {
    return err
    }
    // 这段代码不能放在上面
    if refField.Kind() == reflect.Struct {
    return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
    }

    return nil
}

因为 processField() 函数会对当前的字段解析并设置值,如果这段代码放在上面的话,processsField() 函数就不会执行,也就不会解析当前字段了

在解析 time.Location 类型解析是

location1 := time.UTC
t.Setenv("LOCATION", fmt.Sprintf("%v", location1)) //  fmt.Sprintf("%v", location1) => UTC
location, err := time.LoadLocation(v)

源码:Parse