学习笔记:.env
规范
嘿,大家好👋!这篇文档并不是一个教程,而是我用来辅助自己学习和个人复习的笔记。如果你对完整的内容感兴趣,并且希望通过系统学习来掌握它,那么请直接访问原文章🔗 进行学习吧!
.env
规范
示例代码
err := router.Run(":3000")
上述代码中的端口号是硬编码在代码里的,这并不是一个好的做法。我们需要对此进行优化。类似的需求还包括:
- 数据库连接信息 💾
- Redis 连接信息 🧠
- 验证码复杂度 🔐
- 邮件服务的配置 📧
- 第三方短信的 KEY 和 密钥 📨
解决方案
我们的配置信息将分为两个层级:
- env:环境级别的配置。
- config:应用级别的配置。
.env
文件
一般来说,项目会在多个环境下运行,例如:
- local:本地开发环境(开发者的本地机器)
- testing:自动化测试环境 🤖
- stage:接近生产环境的测试环境,方便其他成员访问和测试(编辑人员、产品经理、项目经理)
- production:线上生产环境 🌍
不同环境下,我们将使用不同的配置。
例如,在 local
环境中,发送短信使用的是测试账号。
而在 production
环境下,我们将使用验证了公司信息的正式账号。
敏感信息处理
.env
文件里通常会存放敏感信息,因此不会将其添加到代码版本库中。如何知道 .env
里有哪些配置项呢?
我们会添加一个 .env.example
文件,配置项放在这里作为占位符,敏感的信息留空,并将此文件提交到版本库中。
当部署到新项目中时,参照此文件创建一个 .env
文件,并对其进行配置即可。
config
目录
config
目录用于存放配置信息,按照独立的逻辑区分单独的配置文件,例如数据库连接信息存放于 config/database.go
文件下。
在 config
里加载 .env
里的配置项,并可设置缺省值。
为什么要有 config
?
既然已经有了 .env
文件,为什么还需要 config
呢?
config
可以提高配置方案的灵活性。在 config
里,我们可以为每个配置项设置默认值;也可以做一些简单的数学运算,或者调用 Go 函数处理默认值。甚至可以为配置项设置一个回调函数。
config
文件需要加入代码版本控制器中,这些代码是固定的。如果需要修改一个 config
配置项,只需修改对应的 .env
文件中的配置项即可。
多个 .env
文件
单个 .env
文件的设计是为了满足一台机器一套环境变量的需求。
多个 .env
文件则是为了满足一台机器上运行多套环境变量的需求。
开发时,除了 local
环境变量,很多时候还需要 testing
相关的环境变量,testing
的配置与 local
有所不同。例如测试时,需要使用不同的数据库,以避免污染开发数据库。
我们可以通过命令行参数,在运行主程序时传递 --env=testing
的参数,程序接收到这个参数后会读取 .env.testing
文件,而不是 .env
文件。
--env
参数的值不需要限制,取到后直接读取对应的文件即可。以下是几个例子:
--env=testing
:读取.env.testing
文件,用以在测试环境使用不同的数据库;--env=production
:读取.env.production
文件,用以在本地环境中调试线上的第三方服务配置信息(短信、邮件)。
viper 包
我们将使用 Viper 包来作为 .env 和 config 信息的基础库。 它支持以下特性:
-
设置默认值(存入时设置)
-
支持格式配置信息的格式包括 JSON、TOML、YAML、HCL、envfile 和 Java properties
-
实时监控和重新读取配置文件(可选)
-
从环境变量中读取
-
从远程配置系统(etcd 或 Consul)读取并监控配置变化
-
从命令行参数读取配置
-
从 buffer 读取配置
-
显式配置值
Viper 的功能比较丰富,这意味着面对多变的需求时我们可以很灵活。且作为明星项目,严格测试、使用广泛,很适合作为我们的配置模块的底层包。
cast
Go 语言是强类型语言,我们在读取 .env 配置信息或者 config 信息时,需要有一个安全的类型保障机制。 Cast 是 Viper 作者的另一个开源包 ,基本上支持所有内置类型之间的转换,下面是几个例子:
// ==== 目标类型为 string 的例子 ====
cast.ToString("mayonegg") // "mayonegg"
cast.ToString(8) // "8"
cast.ToString(8.31) // "8.31"
cast.ToString([]byte("one time")) // "one time"
cast.ToString(nil) // ""
var foo interface{} = "one more time"
cast.ToString(foo) // "one more time"
// ==== 目标类型为 int 的例子 ====
cast.ToInt(8) // 8
cast.ToInt(8.31) // 8
cast.ToInt("8") // 8
cast.ToInt(true) // 1
cast.ToInt(false) // 0
var eight interface{} = 8
cast.ToInt(eight) // 8
cast.ToInt(nil) // 0
-
配置方案的实现
config 包是我们自定的包,对 Viper 第三方库的封装。封装以下逻辑:
- 初始化
- 读取配置文件
- 设置配置项
- 读取配置项
config 包以外的其他项目代码,将对内部使用依赖包 Viper 无感知。 这样做的好处是:如果后续因为某些特殊需求,Viper 无法满足需求,或者 Viper 不再维护,我们需要替换成更加优秀的第三方包时,除了 config 包,项目中的其他代码都不需要修改。
1. 安装依赖包
go get github.com/spf13/cast
go get github.com/spf13/viper
2. config 包
新建文件:
pkg/config/config.go
// Package config 负责配置信息
package config
import (
"gohub/pkg/helpers"
"os"
"github.com/spf13/cast"
viperlib "github.com/spf13/viper" // 自定义包名,避免与内置 viper 实例冲突
)
// viper 库实例
var viper *viperlib.Viper
// ConfigFunc 动态加载配置信息
type ConfigFunc func() map[string]interface{}
// ConfigFuncs 先加载到此数组,loadConfig 再动态生成配置信息
var ConfigFuncs map[string]ConfigFunc
func init() {
// 1. 初始化 Viper 库
viper = viperlib.New()
// 2. 配置类型,支持 "json", "toml", "yaml", "yml", "properties",
// "props", "prop", "env", "dotenv"
viper.SetConfigType("env")
// 3. 环境变量配置文件查找的路径,相对于 main.go
viper.AddConfigPath(".")
// 4. 设置环境变量前缀,用以区分 Go 的系统环境变量
viper.SetEnvPrefix("appenv")
// 5. 读取环境变量(支持 flags)
viper.AutomaticEnv()
ConfigFuncs = make(map[string]ConfigFunc)
}
// InitConfig 初始化配置信息,完成对环境变量以及 config 信息的加载
func InitConfig(env string) {
// 1. 加载环境变量
loadEnv(env)
// 2. 注册配置信息
loadConfig()
}
func loadConfig() {
for name, fn := range ConfigFuncs {
viper.Set(name, fn())
}
}
func loadEnv(envSuffix string) {
// 默认加载 .env 文件,如果有传参 --env=name 的话,加载 .env.name 文件
envPath := ".env"
if len(envSuffix) > 0 {
filepath := ".env." + envSuffix
if _, err := os.Stat(filepath); err == nil {
// 如 .env.testing 或 .env.stage
envPath = filepath
}
}
// 加载 env
viper.SetConfigName(envPath)
if err := viper.ReadInConfig(); err != nil {
panic(err)
}
// 监控 .env 文件,变更时重新加载
viper.WatchConfig()
}
// Env 读取环境变量,支持默认值
func Env(envName string, defaultValue ...interface{}) interface{} {
if len(defaultValue) > 0 {
return internalGet(envName, defaultValue[0])
}
return internalGet(envName)
}
// Add 新增配置项
func Add(name string, configFn ConfigFunc) {
ConfigFuncs[name] = configFn
}
// Get 获取配置项
// 第一个参数 path 允许使用点式获取,如:app.name
// 第二个参数允许传参默认值
func Get(path string, defaultValue ...interface{}) string {
return GetString(path, defaultValue...)
}
func internalGet(path string, defaultValue ...interface{}) interface{} {
// config 或者环境变量不存在的情况
if !viper.IsSet(path) || helpers.Empty(viper.Get(path)) {
if len(defaultValue) > 0 {
return defaultValue[0]
}
return nil
}
return viper.Get(path)
}
// GetString 获取 String 类型的配置信息
func GetString(path string, defaultValue ...interface{}) string {
return cast.ToString(internalGet(path, defaultValue...))
}
// GetInt 获取 Int 类型的配置信息
func GetInt(path string, defaultValue ...interface{}) int {
return cast.ToInt(internalGet(path, defaultValue...))
}
// GetFloat64 获取 float64 类型的配置信息
func GetFloat64(path string, defaultValue ...interface{}) float64 {
return cast.ToFloat64(internalGet(path, defaultValue...))
}
// GetInt64 获取 Int64 类型的配置信息
func GetInt64(path string, defaultValue ...interface{}) int64 {
return cast.ToInt64(internalGet(path, defaultValue...))
}
// GetUint 获取 Uint 类型的配置信息
func GetUint(path string, defaultValue ...interface{}) uint {
return cast.ToUint(internalGet(path, defaultValue...))
}
// GetBool 获取 Bool 类型的配置信息
func GetBool(path string, defaultValue ...interface{}) bool {
return cast.ToBool(internalGet(path, defaultValue...))
}
// GetStringMapString 获取结构数据
func GetStringMapString(path string) map[string]string {
return viper.GetStringMapString(path)
}
上面有一个 helpers.Empty()
方法未定义,现在定义此方法:
pkg/helpers/helpers.go
// Package helpers 存放辅助方法
package helpers
import "reflect"
// Empty 类似于 PHP 的 empty() 函数
func Empty(val interface{}) bool {
if val == nil {
return true
}
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.String, reflect.Array:
return v.Len() == 0
case reflect.Map, reflect.Slice:
return v.Len() == 0 || v.IsNil()
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface())
}
3. 配置配置信息
config/app.go
// Package config 站点配置信息
package config
import "gohub/pkg/config"
func init() {
config.Add("app", func() map[string]interface{} {
return map[string]interface{}{
// 应用名称
"name": config.Env("APP_NAME", "Gohub"),
// 当前环境,用以区分多环境,一般为 local, stage, production, test
"env": config.Env("APP_ENV", "production"),
// 是否进入调试模式
"debug": config.Env("APP_DEBUG", false),
// 应用服务端口
"port": config.Env("APP_PORT", "3000"),
// 加密会话、JWT 加密
"key": config.Env("APP_KEY", "33446a9dcf9ea060a0a6532b166da32f304af0de"),
// 用以生成链接
"url": config.Env("APP_URL", "http://localhost:3000"),
// 设置时区,JWT 里会使用,日志记录里也会使用到
"timezone": config.Env("TIMEZONE", "Asia/Shanghai"),
}
})
}
4. config/config.go
// Package config 存放程序所有的配置信息
package config
// Initialize 触发加载 config 包的所有 init 函数
func Initialize() {
}
5. 创建 .env
文件
.env
APP_ENV=local
APP_KEY=zBqYyQrPNaIUsnRhsGtHLivjqiMjBVLS
APP_DEBUG=true
APP_URL=http://localhost:3000
APP_LOG_LEVEL=debug
APP_PORT=3000Copy
6. 配置初始化
修改 main.go
package main
import (
"flag"
"fmt"
"gohub/bootstrap"
btsConfig "gohub/config"
"gohub/pkg/config"
"github.com/gin-gonic/gin"
)
func init() {
// 加载 config 目录下的配置信息
btsConfig.Initialize()
}
func main() {
// 配置初始化,依赖命令行 --env 参数
var env string
flag.StringVar(&env, "env", "", "加载 .env 文件,如 --env=testing 加载的是 .env.testing 文件")
flag.Parse()
config.InitConfig(env)
// new 一个 Gin Engine 实例
router := gin.New()
// 初始化路由绑定
bootstrap.SetupRoute(router)
// 运行服务
err := router.Run(":" + config.Get("app.port"))
if err != nil {
// 错误处理,端口被占用了或者其他错误
fmt.Println(err.Error())
}
}
7. init 方法
Go 中有两个特殊的函数:
- main 包中的 main 函数,它是所有 Go 可执行程序的入口函数。
- 包级别的 init 函数。
init 函数是一个无参无返回值的函数:
func init() {
...
}
init 函数逻辑:
- Go 运行时会负责在该包初始化时调用它的 init 函数;
- init 不能被显式调用;
- 每个 init 函数在整个 Go 程序生命周期内仅会被执行一次;
- 同一个源文件中的多个 init 函数按声明顺序依次执行。
8. 测试
启动后,可以看到:
[GIN-debug] Listening and serving HTTP on :3000
修改 .env
中的 APP_PORT
项:
APP_PORT=2000
关注命令行窗口是否输出:
[GIN-debug] Listening and serving HTTP on :2000
9. go mod tidy
整理 go.mod
文件:
go mod tidy