返回

Godotenv Introduction

Go第三方库之从.env文件加载环境变量。


简介

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

1
DATABASE=sqlite

.env.production

1
DATABASE=mysql

运行输出:

1
2
3
4
$ go run main.go 
name:  awesome web
version:  0.0.1
database:  mysql

注意事项:

  1. Load接收多个文件名作为参数,如果不传入文件名,默认读取.env文件的内容。
  2. 当指定了环境变量文件,默认的.env文件会失效,除非你加进去。
  3. 如果多个文件中存在同一个键,那么先出现的优先,后出现的不生效。所以,上面输出的database是mysql。
    1. 原因:先出现的已经加载到环境变量中了,默认不会覆盖环境变量中的值。
  4. 使用Load方法加载的环境变量不会覆盖默认的环境变量,要覆盖请用Overload方法。
    1. 使用这个加载,上面输出的database是sqlite。不信你可以试试…
  5. 以上两种方法都会对环境变量的副本,进行添加或修改。后面会介绍,不存入环境变量

注释

.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"])
}

注意:

  1. 环境配置文件中的键值对,会保存在返回的map[string]string中,键名与配置键必须同名。
  2. 直接操作map,简单直接!
  3. 这样就不会将环境配置文件中的变量存入环境变量。
  4. 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"])
}
  1. 通过Unmarshal方法,可以从字符串中读取env文件。存储在返回值map[string]string类型中。
  2. 通过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"])
}
  1. 通过Parse方法,可以中io中读取env文件。从字符串中读取是Unmarshal方法,二者不一样。
  2. 读取的配置都保存在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)
}
  1. 通过Write方法,可以将map中的环境变量,写入到指定文件中。
  2. 通过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

1
DATABASE=sqlite

.env.production

1
DATABASE=mysql

运行输出演示:

 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的源码也比较好读,有时间,有兴趣的童鞋建议一看~


参考

  1. godotenv GitHub 仓库: https://github.com/joho/godotenv
  2. 原文:Go 每日一库之 godotenv