1. 前言
在开发中,我们经常会使用到环境变量,Go语言默认也提供了提取环境变量的内置方法,比如使用 syscall.Getenv(key)
方法,或者是对其的封装 os.LookupEnv(key)
这个方法,都能使我们便捷的获取到环境变量。但是通过内置方法获取到的环境变量返回的是字符串类型的,往往还需要我们对其进行类型转换,为了避免重复工作,今天给大家介绍一款支持将环境变量解析到结构体,并进行类型转换的库。
本文将从库介绍、原理分析、源码解析、不足四方面展开。
2. env库介绍
2.1 安装
Github: https://github.com/caarlos0/env
使用Go mod: github.com/caarlos0/env/v6
2.2 快速入门
package main
import (
"os"
"fmt"
"github.com/caarlos0/env/v6"
)
type MyEnv struct {
Path string `env:"PATH"`
}
func main() {
myenv := MyEnv{}
if err := env.Parse(&myenv); err != nil {
fmt.Printf("%+v\n", err)
os.Exit(-1)
}
fmt.Printf("%+v\n", myenv)
}
复制代码
执行以上代码可以看到如下输出:
{Path:/usr/local/opt/openssl@1.1/bin:/Users/user/.nvm/versions/node/v10.15.3/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Users/user/Library/Python/2.7/bin:/usr/local/go/bin}
复制代码
成功的获取到了我们的环境变量中的PATH,并赋值给了 myenv
这个 MyEnv
结构体实例的 Path
字段。
2.3 标签
这个库是通过反射来解析Tag实现值绑定的,所以需要了解怎么配置结构体的标签。
2.3.1 env
默认必须配置的标签,没有配置则该字段不会被解析。比如在前面我们给 MyEnv
这个结构体的 Path
字段配置了 env
标签,并且值是 PATH
,它代表库将会把环境变量中的 PATH
的值赋给 Path
字段,并且是一个字符串类型。
2.3.1.1 选项
env
标签同时支持一些选项的功能。
2.3.1.1.1 required
required
选项表示这个环境变量必须存在,不存在则会报错,没有此选项的话,在不存在这个环境变量的情况下会采用默认值或空值来作为值。
type MyEnv struct {
GoVersion string `env:"GOVERSION,required"`
}
复制代码
如上,如果环境变量中不存在 GOVERSION
,则会返回错误。
2.3.1.1.2 file
此选项表示将根据环境变量的值去取某个文件,并将这个文件的数据赋值到当前变量,如下:
type MyEnv struct {
Log string `env:"LOG_FILE,file"`
}
复制代码
以上代码将尝试读取环境变量 LOG_FILE
,如此变量值为 ./out.log
,则尝试读取 ./out.log
文件的内容,最终返回给 Log
字段。
2.3.2 envDefault
支持通过此标签设置默认值,以下代码会尝试获取 GOROOT
环境变量,如未设置则默认给 GoRoot
字段赋值为 /usser/local/go
type MyEnv struct {
GoRoot string `env:"GOROOT" envDefault:"/user/local/go"`
}
复制代码
2.3.3 envExpand
此标签标示是否展开值,即如果值中存在变量,则会使用变量的值替换变量,展开会最终的值。如下代码所示,假如要获取的 GOPATH
这环境变量的值为 $HOME/go
,HOME
这个环境变量的值为 /User/user
,则 GoPath
这个字段的最终值为 /User/user/go
。
type MyEnv struct {
GoPath string `env:"GOPATH" envExpand:"true"`
}
复制代码
2.3.4 envSeparator
此标签定义字符串的分隔符,常用于切片类型,默认的分隔符是 ,
,如下代码:
type MyEnv struct {
Ports []int `env:"PORTS" envSeparator:":"`
Users []string `env:"USERS"`
}
复制代码
这时环境变量为:
PORTS=123:456:789
USERS=one,two
复制代码
这样我们可以得到 Ports
字段为 [123, 456, 789]
,Users
字段为 ['one', 'two']
2.4 解析方法
2.4.1 默认支持
env库默认支持了一些类型的解析,这些解析又分为内建的和普通的类型转换。
内建的如下:
bool
int
int8
int16
int32
int64
uint
uint8
uint16
uint32
uint64
float32
float64
string
复制代码
普通的类型转换如下:
time.Duration
url.URL
复制代码
还有一种是通过判定是否支持 encoding.TextUnmarshaler
接口来决定是否进行 UnmarshalText
。
2.4.2 自定义解析
env库中定义了一个 ParserFunc
方法,只要实现这个方法并调用 ParseWithFuncs
传入自定义方法,即可实现自定义的类型解析。
ParserFunc
方法定义如下:
type ParserFunc func(v string) (interface{}, error)
复制代码
自定义解析方法示例如下:
package main
import (
"fmt"
"github.com/caarlos0/env/v6"
"os"
"reflect"
)
type ATest struct {
PWD string `env:"PWD"`
PATH A `env:"PATH"`
}
type A struct {
Value string
}
func ParseA(v string) (interface{}, error) {
return A{v + ":/some/other/path"}, nil
}
func main() {
atest := ATest{}
pm := map[reflect.Type]env.ParserFunc{
reflect.TypeOf(A{}): ParseA,
}
if err := env.ParseWithFuncs(&atest, pm); err != nil {
fmt.Printf("Err:%+v\n", err)
os.Exit(-1)
}
fmt.Printf("%+v\n", atest.PATH.Value)
}
复制代码
以上代码运行最终会输出:
/usr/local/opt/openssl@1.1/bin:/Users/user/.nvm/versions/node/v10.15.3/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Users/user/Library/Python/2.7/bin:/usr/local/go/bin:/some/other/path
复制代码
可以看到 PATH
环境变量的值被添加上了 /some/other/path
最终给到了 Path
字段下的 Value
字段。
3 原理解析
如下图,env库给外部主要暴露了 Parse
和 ParseWithFuncs
两个方法,Parse
方法其实只是封装了一层 ParseWithFuncs
方法,给了他一个空的自定义解析方法Map。ParseWithFuncs
会将传入的自定义解析方法和库中定义的解析方法合并,然后调用 doParse
方法来做解析,doParse
方法遍历结构体下的所有字段,如果为指针型会继续调用 ParseWithFuncs
解析,如果为结构体,则会继续调用 Parse
方法解析。在具体的解析阶段,会先调用 get
方法获取环境变量的值, get
方法会根据前面设定的标签的属性和选项等取得相应的值,然后通过 set
方法将赋值到对应的字段上,在 set
方法中还会判断是否是切片类型,或者是否实现了 TextUnmarshaler
接口,来做对应的处理。
整体上,env库是充分利用了go的反射原理进行相应的标签解析和值类型转换的。
本篇先介绍前两部分,如果喜欢请点赞和关注,下一篇继续源码分析部分:
有疑问加站长微信联系(非本文作者)