Write the Code. Change the World.

12月 29

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

文章来源

https://learnku.com/go

发表评论

电子邮件地址不会被公开。 必填项已用*标注