在一个项目中,环境变量,配置文件这些都是一个很重要的环节。设计好了,体验也会不一般。在 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
