你知道如何在Go语言中愉快的使用环境变量吗?(上)

在开发中,我们经常会使用到环境变量,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/goHOME 这个环境变量的值为 /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库给外部主要暴露了 ParseParseWithFuncs 两个方法, Parse 方法其实只是封装了一层 ParseWithFuncs 方法,给了他一个空的自定义解析方法Map。 ParseWithFuncs 会将传入的自定义解析方法和库中定义的解析方法合并,然后调用 doParse 方法来做解析, doParse 方法遍历结构体下的所有字段,如果为指针型会继续调用 ParseWithFuncs 解析,如果为结构体,则会继续调用 Parse 方法解析。在具体的解析阶段,会先调用 get 方法获取环境变量的值, get 方法会根据前面设定的标签的属性和选项等取得相应的值,然后通过 set 方法将赋值到对应的字段上,在 set 方法中还会判断是否是切片类型,或者是否实现了 TextUnmarshaler 接口,来做对应的处理。

整体上,env库是充分利用了go的反射原理进行相应的标签解析和值类型转换的。

本篇先介绍前两部分,如果喜欢请点赞和关注,下一篇继续源码分析部分。

欢迎关注我们的微信公众号,每天学习Go知识

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章