设置 env tag options

env 这个库提供了一些 tag options

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

required

requried 这个参数使用是方式

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

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
相关推荐
打工仔折腾 AI5 小时前
工业场景下时序库与实时计算一体化架构选型实践对比
java·开发语言·后端·python·性能优化·架构·ai agent 实战
蜗牛互联网5 小时前
长任务多Agent共享文件系统的Manifest交接模式
java·人工智能·后端
苏supper7 小时前
Spring Boot多环境配置与打包运行
spring boot·后端·spring
geovindu7 小时前
rust: Composite Pattern
开发语言·后端·设计模式·rust·组合模式
SearchMan8 小时前
还有人不知道&和&&的区别?
后端
想用offer打牌8 小时前
Personal Agent爆火 - 它到底是个什么
人工智能·后端·ai编程
IT_陈寒8 小时前
Vue的v-if和v-for混用居然是个天坑
前端·人工智能·后端
打工仔折腾 AI10 小时前
从Attention到BERT:双向预训练语言模型到底解决了什么问题
人工智能·后端·python·深度学习·语言模型·bert
lizhongxuan11 小时前
Agent Sandbox: Firecracker 任务生命周期
后端
YYYing.11 小时前
【设计模式系列 (七) 】桥接模式
c++·后端·设计模式·桥接模式