设置 env tag options

env 这个库提供了一些 tag options

  • ,expand: 从环境变量中插入进来,例如 FOO_${BAR}
  • ,file: 环境变量应该是一个文件的路径
  • ,init: 给指针类型的结构体参数设置 nil
  • ,notEmpty:如果环境变量为空会报错
  • ,required:如果环境变量没有设置会报错
  • ,unset: 环境变量在使用会删除掉

required

requried 这个参数使用是方式

第二种使用方式为什么要写一个逗号,是因为 tag 第一个参数是 envkey

go 复制代码
type Config struct {
    IsRequired string `env:"IS_REQUIRED,required"`
}
// 或者
type config struct {
    IsRequired string `env:",required"`
}

在解析 json tag env 时,第一个参数是环境变量的 key ,后面的参数作为 env 的配置参数

go 复制代码
func parseKeyForOption(key string) (string, []string) {
    opts := strings.Split(field.Tag.Get("env"), ",")
    return opts[0], opts[1:]
}

在参数解析时设置判断有没有 required 配置参数

go 复制代码
func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) {
    for _, tag := range tags {
       switch tag {
       case "":
          continue
       case "required":
          result.Required = true
       }
    }
    return result, nil
}

在获取环境变量时,如果传了 required 参数,但是在环境变量中没有找到,就需要抛出 VarIsNotSetError 的错误

go 复制代码
func get(fieldParams FieldParams, opts Options) (val string, err error) {
    if fieldParams.Required && !exists {
       return "", newVarIsNotSetError(fieldParams.Key)
    }
    return value, nil
}

下面两个测试用例就是用来测试 required 参数的

go 复制代码
func TestErrorRequiredNotSet(t *testing.T) {
    type config struct {
       IsRequired string `env:"IS_REQUIRED,required"`
    }
    err := Parse(&config{})
    isErrorWithMessage(t, err, `env: required environment variable "IS_REQUIRED" is not set`)
    isTrue(t, errors.Is(err, VarIsNotSetError{}))
}

func TestNoErrorRequiredSet(t *testing.T) {
    type config struct {
       IsRequired string `env:"IS_REQUIRED,required"`
    }

    cfg := &config{}

    t.Setenv("IS_REQUIRED", "")
    isNoErr(t, Parse(cfg))
    isEqual(t, "", cfg.IsRequired)
}

notEmpty

它的配置和 required 参不多

go 复制代码
func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) {
    for _, tag := range tags {
       switch tag {
       case "":
          continue
       case "notEmpty":
          result.NotEmpty = true
       }
    }
    return result, nil
}

在获取环境变量时,如果传了 notEmpty 参数,但是在环境变量中发现值是空的,就需要抛出 newEmptyVarError 的错误

go 复制代码
func get(fieldParams FieldParams, opts Options) (val string, err error) {
    if fieldParams.NotEmpty && value == "" {
	    return "", newEmptyVarError(fieldParams.Key)
	}
    return value, nil
}

下面四个测试用例就是用来测试 required 参数的

go 复制代码
func TestNoErrorRequiredAndNotEmptySet(t *testing.T) {
    t.Setenv("IS_REQUIRED", "1")
    type config struct {
       IsRequired string `env:"IS_REQUIRED,required,notEmpty"`
    }
    isNoErr(t, Parse(&config{}))
}

func TestNoErrorNotEmptySet(t *testing.T) {
    t.Setenv("IS_REQUIRED", "1")
    type config struct {
       IsRequired string `env:"IS_REQUIRED,notEmpty"`
    }
    isNoErr(t, Parse(&config{}))
}

func TestErrorNotEmptySet(t *testing.T) {
    t.Setenv("IS_REQUIRED", "")
    type config struct {
       IsRequired string `env:"IS_REQUIRED,notEmpty"`
    }
    err := Parse(&config{})
    isErrorWithMessage(t, err, `env: environment variable "IS_REQUIRED" should not be empty`)
    isTrue(t, errors.Is(err, EmptyVarError{}))
}

func TestErrorRequiredAndNotEmptySet(t *testing.T) {
    t.Setenv("IS_REQUIRED", "")
    type config struct {
       IsRequired string `env:"IS_REQUIRED,required,notEmpty"`
    }
    err := Parse(&config{})
    isErrorWithMessage(t, err, `env: environment variable "IS_REQUIRED" should not be empty`)
    isTrue(t, errors.Is(err, EmptyVarError{}))
}

init

init 参数的作用是给指针类型的结构体设置 nil,如果不是设置 nil 的话,在嵌套结构体中会报错

下面代码中在 cfg.InnerStruct 取值 Inner 会报错

go 复制代码
func TestParsesEnvInner_WhenInnerStructPointerIsNil(t *testing.T) {
    type InnerStruct struct {
       Inner  string `env:"innervar"`
       Number uint   `env:"innernum"`
    }
    type ParentStruct struct {
       InnerStruct *InnerStruct
    }

    t.Setenv("innervar", "someinnervalue")
    t.Setenv("innernum", "8")
    cfg := ParentStruct{}
    isNoErr(t, Parse(&cfg))
    isEqual(t, "someinnervalue", cfg.InnerStruct.Inner)
    isEqual(t, uint(8), cfg.InnerStruct.Number)
}

为了解决这个问题,提供了 init 这个 env tag options 参数

这个参数的作用是给 InnerStruct 这个属性进行初始化

go 复制代码
func TestParsesEnvInner_WhenInnerStructPointerIsNil(t *testing.T) {
    type InnerStruct struct {
       Inner  string `env:"innervar"`
       Number uint   `env:"innernum"`
    }
    type ParentStruct struct {
       InnerStruct *InnerStruct `env:",init"`
    }

    t.Setenv("innervar", "someinnervalue")
    t.Setenv("innernum", "8")
    cfg := ParentStruct{}
    isNoErr(t, Parse(&cfg))
    isEqual(t, "someinnervalue", cfg.InnerStruct.Inner)
    isEqual(t, uint(8), cfg.InnerStruct.Number)
}

因为 cfg1 这个结构体没有初始化 InnerStruct ,所以会报错,需要用 init 参数初始化 InnerStruct

cfg2 结构体是显示初始化了 InnerStruct 结构体

go 复制代码
 type ParentStruct struct {
    InnerStruct *InnerStruct
}

cfg1 := &ParentStruct{}
cfg2 := &ParentStruct{
	InnerStruct: &InnerStruct{}
}

使用 init 初始化结构体地址

go 复制代码
if params.Init && isStructPtr(refField) && refField.IsNil() {
    refField.Set(reflect.New(refField.Type().Elem()))
    refField = refField.Elem()
}

expand

expand 这个参数是利用 os.Expand() 这个 api 实现的

原理是将 ${HOST} 或者 $HOME 传递给 os.Expand() ,会有一个解析参数将 HOST 成想要的值

go 复制代码
mapping := func(varName string) string {
    switch varName {
    case "HOST":
       return "localhost"
    case "PORT":
       return "3000"
    default:
       return ""
    }
}
result := os.Expand("${HOST}:${PORT}", mapping)
fmt.Println(result) // localhost:3000

如果不是 ${HOST} 格式的值,会被原封不动的返回回去

go 复制代码
result2 := os.Expand("HOST", mapping)
fmt.Println(result2) // HOST

所以 expand 的具体实现方法

go 复制代码
if fieldParams.Expand {
    val = os.Expand(val, opts.getRawEnv)
}
func (opts *Options) getRawEnv(s string) string {
    val := opts.rawEnvVars[s]
    if val == "" {
       val = opts.Environment[s]
    }
    return os.Expand(val, opts.getRawEnv)
}

它的测试用例

go 复制代码
func TestParseExpandOption(t *testing.T) {
    type config struct {
       Host        string `env:"HOST" envDefault:"localhost"`
       Port        int    `env:"PORT,expand" envDefault:"3000"`
       SecretKey   string `env:"SECRET_KEY,expand"`
       ExpandKey   string `env:"EXPAND_KEY"`
       CompoundKey string `env:"HOST_PORT,expand" envDefault:"${HOST}:${PORT}"`
       Default     string `env:"DEFAULT,expand" envDefault:"def1"`
    }

    t.Setenv("HOST", "localhost")
    t.Setenv("PORT", "3000")
    t.Setenv("EXPAND_KEY", "qwerty12345")
    t.Setenv("SECRET_KEY", "${EXPAND_KEY}")

    cfg := config{}
    err := Parse(&cfg)

    isNoErr(t, err)
    isEqual(t, "localhost", cfg.Host)
    isEqual(t, 3000, cfg.Port)
    isEqual(t, "qwerty12345", cfg.SecretKey)
    isEqual(t, "qwerty12345", cfg.ExpandKey)
    isEqual(t, "localhost:3000", cfg.CompoundKey)
    isEqual(t, "def1", cfg.Default)
}

func TestParseExpandWithDefaultOption(t *testing.T) {
    type config struct {
       Host            string `env:"HOST" envDefault:"localhost"`
       Port            int    `env:"PORT,expand" envDefault:"3000"`
       OtherPort       int    `env:"OTHER_PORT" envDefault:"4000"`
       CompoundDefault string `env:"HOST_PORT,expand" envDefault:"${HOST}:${PORT}"`
       SimpleDefault   string `env:"DEFAULT,expand" envDefault:"def1"`
       MixedDefault    string `env:"MIXED_DEFAULT,expand" envDefault:"$USER@${HOST}:${OTHER_PORT}"`
       OverrideDefault string `env:"OVERRIDE_DEFAULT,expand"`
       DefaultIsExpand string `env:"DEFAULT_IS_EXPAND,expand" envDefault:"$THIS_IS_EXPAND"`
       NoDefault       string `env:"NO_DEFAULT,expand"`
    }
    t.Setenv("HOST", "localhost")
    t.Setenv("PORT", "3000")
    t.Setenv("OTHER_PORT", "5000")
    t.Setenv("USER", "jhon")
    t.Setenv("THIS_IS_USED", "this is used instead")
    t.Setenv("OVERRIDE_DEFAULT", "msg: ${THIS_IS_USED}")
    t.Setenv("THIS_IS_EXPAND", "msg: ${THIS_IS_USED}")
    t.Setenv("NO_DEFAULT", "$PORT:$OTHER_PORT")

    cfg := config{}
    err := Parse(&cfg)

    isNoErr(t, err)
    isEqual(t, "localhost", cfg.Host)
    isEqual(t, 3000, cfg.Port)
    isEqual(t, 5000, cfg.OtherPort)
    isEqual(t, "localhost:3000", cfg.CompoundDefault)
    isEqual(t, "def1", cfg.SimpleDefault)
    isEqual(t, "jhon@localhost:5000", cfg.MixedDefault)
    isEqual(t, "msg: this is used instead", cfg.OverrideDefault)
    isEqual(t, "3000:5000", cfg.NoDefault)
}

unset

unset 这个参数的作用是将已经使用的环境变量删除

go 复制代码
os.Setenv("PASSWORD", "superSecret")
os.Unsetenv("PASSWORD")
unset, exists := os.LookupEnv("PASSWORD")
fmt.Println(unset, exists)  // "", false

实现方式

go 复制代码
if fieldParams.Unset {
    defer os.Unsetenv(fieldParams.Key)
}

测试用例

go 复制代码
func TestParseUnsetRequireOptions(t *testing.T) {
    type config struct {
       Password string `env:"PASSWORD,unset,required"`
    }
    cfg := config{}

    err := Parse(&cfg)
    isErrorWithMessage(t, err, `env: required environment variable "PASSWORD" is not set`)
    isTrue(t, errors.Is(err, VarIsNotSetError{}))
    os.Setenv("PASSWORD", "superSecret")
    isNoErr(t, Parse(&cfg))

    isEqual(t, "superSecret", cfg.Password)
    unset, exists := os.LookupEnv("PASSWORD")
    isEqual(t, "", unset)
    isEqual(t, false, exists)
}

file

file 参数用来设置环境变量的值是某个文件的路径

go 复制代码
dir, _ := os.Getwd()
file := filepath.Join(dir, "sec_key")   // file 是文件路径
isNoErr(t, os.WriteFile(file, []byte("secret"), 0o660))

t.Setenv("SECRET_KEY", file)

实现:

go 复制代码
if fieldParams.LoadFile && val != "" {
    filename := val
    val, err = getFromFile(filename)
    if err != nil {
       return "", newLoadFileContentError(filename, fieldParams.Key, err)
    }
}
func getFromFile(filename string) (value string, err error) {
    b, err := os.ReadFile(filename)
    return string(b), err
}

测试用例

go 复制代码
func TestFile(t *testing.T) {
    type config struct {
       SecretKey string `env:"SECRET_KEY,file"`
    }

    dir := t.TempDir()
    file := filepath.Join(dir, "sec_key")
    isNoErr(t, os.WriteFile(file, []byte("secret"), 0o660))

    t.Setenv("SECRET_KEY", file)

    cfg := config{}
    isNoErr(t, Parse(&cfg))
    isEqual(t, "secret", cfg.SecretKey)
}

func TestFileNoParam(t *testing.T) {
    type config struct {
       SecretKey string `env:"SECRET_KEY,file"`
    }

    cfg := config{}
    err := Parse(&cfg)
    isNoErr(t, err)
}

func TestFileNoParamRequired(t *testing.T) {
    type config struct {
       SecretKey string `env:"SECRET_KEY,file,required"`
    }

    err := Parse(&config{})
    isErrorWithMessage(t, err, `env: required environment variable "SECRET_KEY" is not set`)
    isTrue(t, errors.Is(err, VarIsNotSetError{}))
}

func TestFileBadFile(t *testing.T) {
    type config struct {
       SecretKey string `env:"SECRET_KEY,file"`
    }

    filename := "not-a-real-file"
    t.Setenv("SECRET_KEY", filename)

    oserr := "no such file or directory"
    if runtime.GOOS == "windows" {
       oserr = "The system cannot find the file specified."
    }

    err := Parse(&config{})
    isErrorWithMessage(t, err, fmt.Sprintf("env: could not load content of file %q from variable SECRET_KEY: open %s: %s", filename, filename, oserr))
    isTrue(t, errors.Is(err, LoadFileContentError{}))
}

func TestFileWithDefault(t *testing.T) {
    type config struct {
       SecretKey string `env:"SECRET_KEY,file,expand" envDefault:"${FILE}"`
    }

    dir := t.TempDir()
    file := filepath.Join(dir, "sec_key")
    isNoErr(t, os.WriteFile(file, []byte("secret"), 0o660))

    t.Setenv("FILE", file)

    cfg := config{}
    isNoErr(t, Parse(&cfg))
    isEqual(t, "secret", cfg.SecretKey)
}

源码:

  1. required
  2. notEmpty
  3. init
  4. expand
  5. unset
  6. file
相关推荐
一只叫煤球的猫2 分钟前
MySQL 8.0 SQL优化黑科技,面试官都不一定知道!
后端·sql·mysql
写bug写bug1 小时前
如何正确地对接口进行防御式编程
java·后端·代码规范
不超限1 小时前
Asp.net core 使用EntityFrame Work
后端·asp.net
豌豆花下猫1 小时前
Python 潮流周刊#105:Dify突破10万星、2025全栈开发的最佳实践
后端·python·ai
忆雾屿2 小时前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
Undoom4 小时前
🔥支付宝百宝箱新体验!途韵归旅小帮手,让高铁归途变旅行
后端
油腻中年李大鹅4 小时前
使用scheduler-plugins实现自定义调度器
kubernetes·go
不超限4 小时前
Asp.net Core 通过依赖注入的方式获取用户
后端·asp.net