设置 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
相关推荐
石榴树下3 分钟前
00. 马里奥的 OAuth 2 和 OIDC 历险记
后端
uhakadotcom4 分钟前
开源:subdomainpy快速高效的 Python 子域名检测工具
前端·后端·面试
似水流年流不尽思念21 分钟前
容器化技术了解吗?主要解决什么问题?原理是什么?
后端
Java水解22 分钟前
Java中的四种引用类型详解:强引用、软引用、弱引用和虚引用
java·后端
i听风逝夜22 分钟前
看好了,第二遍,SpringBoot单体应用真正的零停机无缝更新代码
后端
柏油1 小时前
可视化 MySQL binlog 监听方案
数据库·后端·mysql
舒一笑2 小时前
Started TttttApplication in 0.257 seconds (没有 Web 依赖导致 JVM 正常退出)
jvm·spring boot·后端
M1A12 小时前
Java Enum 类:优雅的常量定义与管理方式(深度解析)
后端
AAA修煤气灶刘哥2 小时前
别再懵了!Spring、Spring Boot、Spring MVC 的区别,一篇讲透
后端·面试
柏油3 小时前
MySQL 字符集 utf8 与 utf8mb4
数据库·后端·mysql