简介
twelve-factor应用提倡将配置存储在环境变量中。任何从开发环境切换到生产环境时需要修改的东西都从代码抽取到环境变量里。 但是在实际开发中,如果同一台机器运行多个项目,设置环境变量容易冲突,不实用。godotenv库从.env
文件中读取配置, 然后存储到程序的环境变量中。在代码中可以使用读取非常方便。godotenv
源于一个 Ruby 的开源项目dotenv。
快速使用
第三方库需要先安装:
1
| $ go get -u github.com/joho/godotenv
|
后使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
fmt.Println("name:", os.Getenv("name"))
fmt.Println("age:", os.Getenv("AGE"))
}
|
然后在可执行程序相同目录下,添加一个.env
文件(可以给ide安装插件,检查.env
文件语法,安装用的人多的。如GoLand:.env files support):
1
2
| NAME=arlettebrook
AGE=18
|
运行程序,输出:
1
2
3
| $ go run main.go
name: arlettebrook
age: 18
|
可见,使用非常方便。默认情况下,godotenv
读取项目根目录下的.env
文件,文件中使用key=value
的格式,每行一个键值对。 调用godotenv.Load()
即可加载,可直接调用os.Getenv("key")
读取,os.Getenv
是用来读取环境变量的:windows上不区分大小写,但环境变量通常都是大写,建议用大写。没找到返回空字符串。
1
2
3
4
5
6
7
8
9
10
| package main
import (
"fmt"
"os"
)
func main() {
fmt.Println(os.Getenv("GOPATH")) // 会返回GOPAHT环境变量的值
}
|
基本使用
自动加载
如果你有程序员的优良传统——懒,你可能连Load
方法都不想自己调用。没关系,godotenv
给你懒的权力!
导入github.com/joho/godotenv/autoload
,配置会自动读取:
1
2
3
4
5
6
7
8
9
10
11
12
13
| package main
import (
"fmt"
"os"
_ "github.com/joho/godotenv/autoload"
)
func main() {
fmt.Println("name: ", os.Getenv("NAME"))
fmt.Println("age: ", os.Getenv("AGE"))
}
|
注意,由于代码中没有显式用到godotenv
库,需要使用空导入,即导入时包名前添加一个_
。作用:自动调用init
函数。
看autoload
包的源码,其实就是库帮你调用了Load
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| package autoload
/*
You can just read the .env file on import just by doing
import _ "github.com/joho/godotenv/autoload"
And bob's your mother's brother
*/
import "github.com/joho/godotenv"
func init() {
godotenv.Load()
}
|
仔细看注释,程序员的恶趣味😂!
加载自定义文件
默认情况下,加载的是项目根目录下的.env
文件。当然我们可以加载任意名称的文件,文件也不必以.env
为后缀:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| package main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load("common", ".env.production", ".env.development")
if err != nil {
log.Fatal(err)
}
fmt.Println("name: ", os.Getenv("NAME"))
fmt.Println("version: ", os.Getenv("VERSION"))
fmt.Println("database: ", os.Getenv("DATABASE"))
}
|
common
文件内容:
1
2
| NAME=awesome web
VERSION=0.0.1
|
.env.development
:
.env.production
:
运行输出:
1
2
3
4
| $ go run main.go
name: awesome web
version: 0.0.1
database: mysql
|
注意事项:
Load
接收多个文件名作为参数,如果不传入文件名,默认读取.env
文件的内容。- 当指定了环境变量文件,默认的
.env
文件会失效,除非你加进去。 - 如果多个文件中存在同一个键,那么先出现的优先,后出现的不生效。所以,上面输出的
database
是mysql。- 原因:先出现的已经加载到环境变量中了,默认不会覆盖环境变量中的值。
- 使用
Load
方法加载的环境变量不会覆盖默认的环境变量,要覆盖请用Overload
方法。- 使用这个加载,上面输出的
database
是sqlite。不信你可以试试…
- 以上两种方法都会对环境变量的副本,进行添加或修改。后面会介绍,不存入环境变量。
注释
.env
文件中可以添加注释,注释以#
开始,直到该行结束。
1
2
3
4
| # app name
NAME=awesome web
# current version
VERSION=0.0.1
|
YAML
.env
文件还可以使用 YAML 格式:
1
2
| NAME: 'awesome web'
VERSION: 0.0.1
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| package main
import (
"fmt"
"os"
_ "github.com/joho/godotenv/autoload"
)
func main() {
fmt.Println("name: ", os.Getenv("NAME"))
fmt.Println("version: ", os.Getenv("VERSION"))
}
|
运行输出:
1
2
3
| $ go run main.go
name: awesome web
version: 0.0.1
|
注意:yaml格式不支持嵌套。官方解释:支持 YAML(ish) 风格。
不存入环境变量
从文件读取
godotenv
允许不将.env
文件内容存入环境变量,使用godotenv.Read()
返回一个map[string]string
,可直接使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package main
import (
"fmt"
"log"
"github.com/joho/godotenv"
)
func main() {
myEnv, err := godotenv.Read()
if err != nil {
log.Fatal(err)
}
fmt.Println("name: ", myEnv["NAME"])
fmt.Println("version: ", myEnv["VERSION"])
}
|
注意:
- 环境配置文件中的键值对,会保存在返回的
map[string]string
中,键名与配置键必须同名。 - 直接操作
map
,简单直接! - 这样就不会将环境配置文件中的变量存入环境变量。
Read
可以接收文件路径,用于指定配置文件。默认./.env
,与Load
一致。
从string, byte
中读取配置
除了读取文件,还可以从string
中读取配置,它不会修改环境:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| package main
import (
"fmt"
"log"
"github.com/joho/godotenv"
)
func main() {
stringContent := `
name: awesome web
version: 0.0.1
`
byteContent := []byte(`
name: awesome web byte
version: 1.0.1`)
myEnvWithString, err := godotenv.Unmarshal(stringContent)
myEnvWithByte, err := godotenv.UnmarshalBytes(byteContent)
if err != nil {
log.Fatal(err)
}
fmt.Println("name: ", myEnvWithString["name"])
fmt.Println("version: ", myEnvWithString["version"])
fmt.Println("--------UnmarshalBytes--------")
fmt.Println("name: ", myEnvWithByte["name"])
fmt.Println("version: ", myEnvWithByte["version"])
}
|
- 通过
Unmarshal
方法,可以从字符串中读取env文件。存储在返回值map[string]string类型中。 - 通过
UnmarshalBytes
方法,可以从字节切片中读取env文件。也是存储在map中。
运行输出:
1
2
3
4
5
6
| $ go run main.go
name: awesome web
version: 0.0.1
--------UnmarshalBytes--------
name: awesome web byte
version: 1.0.1
|
从io.Reader
获取配置
除了以上方法外,还可以从io.Reader
中读取env文件。这个也不会修改环境。
只要实现了io.Reader
接口,就能作为数据源。可以从文件(os.File
),网络(net.Conn
),bytes.Buffer
等多种来源读取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| package main
import (
"bytes"
"fmt"
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
file, _ := os.Open(".env")
myEnv, err := godotenv.Parse(file)
if err != nil {
log.Fatal(err)
}
fmt.Println("name: ", myEnv["NAME"])
fmt.Println("version: ", myEnv["VERSION"])
buf := bytes.NewBuffer([]byte{})
buf.WriteString("name: awesome web @buffer")
buf.Write([]byte{'\n'})
buf.WriteString("version: 0.0.1 @buffer")
myEnv, err = godotenv.Parse(buf)
if err != nil {
log.Fatal(err)
}
fmt.Println("name: ", myEnv["name"])
fmt.Println("version: ", myEnv["version"])
}
|
- 通过
Parse
方法,可以中io中读取env文件。从字符串中读取是Unmarshal
方法,二者不一样。 - 读取的配置都保存在map中,没有存入环境变量。map键与配置键必须同名。通过
os.Getenv
指定的可以不区分大小写,但建议大写。
运行输出:
1
2
3
4
5
| $ go run main.go
name: awesome web
version: 0.0.1
name: awesome web @buffer
version: 0.0.1 @buffer
|
生成.env
文件or字符串
可以通过程序生成一个.env
文件的内容,可以直接写入到文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| package main
import (
"bytes"
"fmt"
"log"
"github.com/joho/godotenv"
)
func main() {
buf := bytes.NewBuffer([]byte{})
buf.WriteString("NAME=awesome web @write")
buf.WriteByte('\n')
buf.WriteString("VERSION=0.0.1 @write")
envMap, err := godotenv.Parse(buf)
if err != nil {
log.Fatal(err)
}
err = godotenv.Write(envMap, "./write.env")
stringEnv, err := godotenv.Marshal(envMap)
if err != nil {
log.Fatal(err)
}
fmt.Println(stringEnv)
}
|
- 通过
Write
方法,可以将map中的环境变量,写入到指定文件中。 - 通过
Marshal
方法,可以将map中的环境变量,序列化成字符串
运行会在当前目录下生成write.env
文件:
1
2
| NAME="awesome web @write"
VERSION="0.0.1 @write"
|
TTY输出:
1
2
3
| $ go run main.go
NAME="awesome web @write"
VERSION="0.0.1 @write"
|
命令行模式
godotenv
还提供了一个命令行的模式。要使用它,先要确保命令安装到$GOPATH/bin
目录下:
1
| $ go install github.com/joho/godotenv/cmd/godotenv@latest
|
这个命令行程序,源码很简单,在github.com/joho/godotenv/cmd/godotenv
路径下,用flag
库解析的命令行参数。作用是读取env文件,写入环境变量中,不用在程序中调用godotenv。最后是通过Exec
方法调用Load
方法实现的。感兴趣的可以自己去看一下。
安装后之后可以查看帮助信息,大致为:
1
2
| $ godotenv -h
godotenv [-o] [-f ENV_FILE_PATHS] COMMAND_ARGS
|
- -o:是否覆盖环境变量,默认false
- -f:字段env文件,默认./.env
- 剩余参数:启动的程序
示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
| package main
import (
"fmt"
"os"
)
func main() {
fmt.Println(os.Getenv("NAME"))
fmt.Println(os.Getenv("VERSION"))
fmt.Println(os.Getenv("USERNAME"))
}
|
./.env
文件:
1
2
3
| NAME: 'awesome web'
VERSION: 0.0.1
USERNAME: arlettebrook
|
使用godotenv命令启动程序,演示如下:
1
2
3
4
5
6
7
8
9
| $ godotenv go run main.go
awesome web
0.0.1
Lenovo
$ godotenv -o go run main.go
awesome web
0.0.1
arlettebrook
|
第一次没有覆盖USERNAME,第二次覆盖了。
通过godotenv命令行程序,我们可以不用再自己的程序中调用godotenv读取env文件。
指定环境启动
实践中,一般会根据APP_ENV
环境变量的值加载不同的文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| package main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
env := os.Getenv("LEARN_ENV")
if env == "" {
env = "development"
}
err := godotenv.Load(".env." + env)
if err != nil {
log.Fatal(err)
}
err = godotenv.Load()
if err != nil {
log.Fatal(err)
}
fmt.Println("name: ", os.Getenv("NAME"))
fmt.Println("version: ", os.Getenv("VERSION"))
fmt.Println("database: ", os.Getenv("DATABASE"))
}
|
我们先读取环境变量LEARN_ENV
,然后读取对应的.env.
+ env,最后读取默认的.env
文件。
前面也提到过,先读取到的优先。我们可以在默认的.env
文件中配置基础信息和一些默认的值, 如果在开发/测试/生产环境需要修改,那么在对应的.env.development/.env.test/.env.production
文件中再配置一次即可。
.env
文件内容:
1
2
3
| NAME: 'awesome web'
VERSION: 0.0.1
DATABASE: mongodb
|
.env.development
:
.env.production
:
运行输出演示:
1
2
3
4
5
6
7
8
9
10
11
| # 默认是开发环境
$ go run main.go
name: awesome web
version: 0.0.1
database: sqlite # 用Load不会覆盖,所以表示mongodb
# 设置为生成环境
$ LEARN_ENV=production go run main.go
name: awesome web
version: 0.0.1
database: mysql
|
一点源码
(其实你应该提前看一下源码~)
godotenv
读取文件内容,为什么可以使用os.Getenv
访问:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // github.com/joho/godotenv/godotenv.go
func loadFile(filename string, overload bool) error {
envMap, err := readFile(filename)
if err != nil {
return err
}
currentEnv := map[string]bool{}
rawEnv := os.Environ()
for _, rawEnvLine := range rawEnv {
key := strings.Split(rawEnvLine, "=")[0]
currentEnv[key] = true
}
for key, value := range envMap {
if !currentEnv[key] || overload {
_ = os.Setenv(key, value)
}
}
return nil
}
|
因为godotenv
调用os.Setenv
将键值对设置到环境变量中了。就是在运行的时候修改了环境变量。
总结
本文介绍了godotenv
库的基础和高级用法。godotenv
的源码也比较好读,有时间,有兴趣的童鞋建议一看~
参考
- godotenv GitHub 仓库: https://github.com/joho/godotenv
- 原文:Go 每日一库之 godotenv