在一个项目中,环境变量,配置文件这些都是一个很重要的环节。设计好了,体验也会不一般。在 go 中,使用 viper 来构造一个这样的配置环境。
https://github.com/spf13/viper
开始之前
- 在开始之前,我们已经熟悉了 laravel 的配置文件的方式。这里,我们仿照 laravel 的结构方式来组织我们的代码。使用 .env 文件来放置重要配置信息。当然也有 .env.example 。.env 不会加入版本控制中,.env.example 加入版本控制中。
-
go 的 init 函数。相关联文件中, init 函数总是会先执行的。init 函数要慎用,仔细用。导入顺序也很有关系。
-
一个项目中 MVC 是指的是业务逻辑的代码,而除了支撑业务逻辑代码以外,还有底层的辅助代码。例如路由加载、数据库初始化等。在我们的项目中,遵循 Go 社区的惯例,这些底层代码我们会归类为各自的包,并放置于 pkg 目录下。pkg 目录下的包,我们会尽量保持其独立性,这样方便在其他项目中复用。但是最重要的,当前还是以服务 goblog 项目为主。
开始
我们先初始化一个 mod
go mod init vini123.com
代码结构如下:
❯ tree
.
├── bootstrap
│ └── db.go
├── config
│ ├── app.go
│ ├── config.go
│ ├── database.go
│ └── session.go
├── go.mod
├── go.sum
├── main.go
└── pkg
├── config
│ └── config.go
└── logger
└── logger.go
先看 pkg/config/config.go 文件:
package config
import (
"github.com/fsnotify/fsnotify"
"github.com/spf13/cast"
"github.com/spf13/viper"
"vini123.com/pkg/logger"
)
// Viper Viper 库实例
var Viper *viper.Viper
// StrMap 简写 —— map[string]interface{}
type StrMap map[string]interface{}
// init() 函数在 import 的时候立刻被加载
func init() {
// 1. 初始化 Viper 库
Viper = viper.New()
// 2. 设置文件名称
Viper.SetConfigName(".env")
// 3. 配置类型,支持 "json", "toml", "yaml", "yml", "properties",
// "props", "prop", "env", "dotenv"
Viper.SetConfigType("env")
// 4. 环境变量配置文件查找的路径,相对于 main.go
Viper.AddConfigPath(".")
// 5. 开始读根目录下的 .env 文件,读不到会报错
err := Viper.ReadInConfig()
logger.LogError(err)
// 6. 设置环境变量前缀,用以区分 Go 的系统环境变量
Viper.SetEnvPrefix("appenv")
// 7. Viper.Get() 时,优先读取环境变量
Viper.AutomaticEnv()
Viper.OnConfigChange(func(e fsnotify.Event) {
logger.Info(Viper.AllSettings())
})
}
// Env 读取环境变量,支持默认值
func Env(envName string, defaultValue ...interface{}) interface{} {
if len(defaultValue) > 0 {
return Get(envName, defaultValue[0])
}
return Get(envName)
}
// Add 新增配置项
func Add(name string, configuration map[string]interface{}) {
Viper.Set(name, configuration)
}
// Get 获取配置项,允许使用点式获取,如:app.name
func Get(path string, defaultValue ...interface{}) interface{} {
// 不存在的情况
if !Viper.IsSet(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(Get(path, defaultValue...))
}
// GetInt 获取 Int 类型的配置信息
func GetInt(path string, defaultValue ...interface{}) int {
return cast.ToInt(Get(path, defaultValue...))
}
// GetInt64 获取 Int64 类型的配置信息
func GetInt64(path string, defaultValue ...interface{}) int64 {
return cast.ToInt64(Get(path, defaultValue...))
}
// GetUint 获取 Uint 类型的配置信息
func GetUint(path string, defaultValue ...interface{}) uint {
return cast.ToUint(Get(path, defaultValue...))
}
// GetBool 获取 Bool 类型的配置信息
func GetBool(path string, defaultValue ...interface{}) bool {
return cast.ToBool(Get(path, defaultValue...))
}
再来看看 pkg/logger/logger.go :
package logger
import "log"
func LogError(err error) {
if err != nil {
log.Println(err)
}
}
func Info(value interface{}) {
if value != nil {
log.Println(value)
}
}
下边进入配置文件的构造环节中了。在这里,用到了三层。一层是 .env 文件,一层是 config 下边的配置文件,还有一层就是 pkg/config 文件。
对于 config 下边的配置文件,我们先规划好了。比如 app.go 对应项目基本配置,database.go 对应数据库相关的配置(比如 mysql,redis)这些。
config/config.go,该文件仅仅是用来触发该目录下的所有文件的 init 方法。Initialize 是自己定义的一个方法。当然你用其他的方法名也是可以的。
package config
import "fmt"
func Initialize() {
// 触发加载本目录下其他文件中的 init 方法
fmt.Println("this is initialize")
}
config/database.go
package config
import "vini123.com/pkg/config"
func init() {
config.Add("database", config.StrMap{
"mysql": map[string]interface{}{
"default": map[string]interface{}{
// 数据库连接信息
"host": config.Env("DB_HOST", "127.0.0.1"),
"port": config.Env("DB_PORT", "3306"),
"database": config.Env("DB_DATABASE", "goblog"),
"username": config.Env("DB_USERNAME", ""),
"password": config.Env("DB_PASSWORD", ""),
"charset": "utf8mb4",
// 连接池配置
"max_idle_connections": config.Env("DB_MAX_IDLE_CONNECTIONS", 100),
"max_open_connections": config.Env("DB_MAX_OPEN_CONNECTIONS", 25),
"max_life_seconds": config.Env("DB_MAX_LIFE_SECONDS", 5*60),
},
},
"redis": map[string]interface{}{
"default": map[string]interface{}{
"url": config.Env("REDIS_URL"),
"host": config.Env("REDIS_HOST", "127.0.0.1"),
"password": config.Env("REDIS_PASSWORD"),
"port": config.Env("REDIS_PORT", "6379"),
"database": config.Env("REDIS_DB", "0"),
},
},
})
}
config/app.go
package config
import "vini123.com/pkg/config"
func init() {
config.Add("app", config.StrMap{
// 应用名称,暂时没有使用到
"name": config.Env("APP_NAME", "GoBlog"),
// 当前环境,用以区分多环境
"env": config.Env("APP_ENV", "production"),
// 是否进入调试模式
"debug": config.Env("APP_DEBUG", false),
// 应用服务端口
"port": config.Env("APP_PORT", "3000"),
// gorilla/sessions 在 Cookie 中加密数据时使用
"key": config.Env("APP_KEY", "33446a9dcf9ea060a0a6532b166da32f304af0de"),
})
}
config/session.go
package config
import "vini123.com/pkg/config"
func init() {
config.Add("session", config.StrMap{
// 目前只支持 cookie
"default": config.Env("SESSION_DRIVER", "cookie"),
// 会话的 cookie 名称
"session_name": config.Env("SESSION_NAME", "goblog-session"),
})
}
基本的配置好了。我们在 main 中引入并初始化它。
main.go
package main
import (
"fmt"
"vini123.com/bootstrap"
"vini123.com/config"
)
func init() {
config.Initialize()
}
func main() {
fmt.Println("this is main")
bootstrap.SetupDB()
}
最后,我们要使用配置文件。如果仅仅只是配置了不使用,那就没意思了。
bootstrap/db.go
package bootstrap
import (
"fmt"
"vini123.com/pkg/config"
)
func SetupDB() {
var (
host = config.GetString("database.mysql.default.host")
port = config.GetString("database.mysql.default.port")
database = config.GetString("database.mysql.default.database")
username = config.GetString("database.mysql.default.username")
password = config.GetString("database.mysql.default.password")
charset = config.GetString("database.mysql.default.charset")
)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=%t&loc=%s", username, password, host, port, database, charset, true, "Local")
fmt.Println(dsn)
}
运行康康。
go run main.go
会看到:
this is initialize
this is main
homestead:secret@tcp(127.0.0.1:3306)/goblog?charset=utf8mb4&parseTime=true&loc=Local