开始前的准备
1
| $ go get github.com/spf13/viper
|
viper是一个相当常用的配置管理包,功能相当强大。但是如果之前没有接触过这个包,第一次学习可能感到疑惑,你可以根据自己的情况去先学习viper的用法或者保持一定的疑惑。
1
| $ go get github.com/spf13/cast
|
一个非常方便的类型转换包
编码
编写viper工具包
pkg/config/config.go
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| package config
import ( "github.com/spf13/cast" viperlib "github.com/spf13/viper" "go-api-practice/helpers" "os" )
var viper *viperlib.Viper
type ConfigFunc func() map[string]interface{}
var ConfigFuncs map[string]ConfigFunc
func init() { viper = viperlib.New()
viper.SetConfigType("env")
viper.AddConfigPath(".")
viper.SetEnvPrefix("appenv")
viper.AutomaticEnv()
ConfigFuncs = make(map[string]ConfigFunc) }
func InitConfig(env string) { loadEnv(env) loadConfig() }
func loadConfig() { for name, fn := range ConfigFuncs { viper.Set(name, fn()) } }
func loadEnv(envSuffix string) { envPath := ".env" if len(envSuffix) > 0 { filePath := envPath + envSuffix if _, err := os.Stat(filePath); err != nil { envPath = filePath } }
viper.SetConfigName(envPath) if err := viper.ReadInConfig(); err != nil { panic(err) }
viper.WatchConfig() } func Env(envName string, defaultValue ...interface{}) interface{} { if len(defaultValue) > 0 { return internalGet(envName, defaultValue[0]) } return internalGet(envName) }
func Add(name string, configFn ConfigFunc) { ConfigFuncs[name] = configFn }
func Get(path string, defaultValue ...interface{}) string { return GetString(path, defaultValue...) }
func internalGet(path string, defaultValue ...interface{}) interface{} { if !viper.IsSet(path) || helpers.Empty(viper.Get(path)) { if len(defaultValue) > 0 { return defaultValue[0] } return nil } return viper.Get(path) }
func GetString(path string, defaultValue ...interface{}) string { return cast.ToString(internalGet(path, defaultValue...)) } func GetInt(path string, defaultValue ...interface{}) int { return cast.ToInt(internalGet(path, defaultValue...)) } func GetBool(path string, defaultValue ...interface{}) bool { return cast.ToBool(internalGet(path, defaultValue...)) }
|
这里内容有点复杂,我一点点慢慢讲
viper实例
这里我们用New()
方法去初始化一个viper实例
,这样初始化的viper
会有一些默认的配置,我们在使用的要注意,特别的,不要写成一下这种形式。
1 2 3 4 5 6 7 8 9
| func New() *Viper { v := new(Viper) v.keyDelim = "." v.configName = "config" . . . return v }
|
如果你看过viper的入门教程,那么你就会明白这些配置的作用,如果你现在还不能理解,我会在下面用到的时候在加以说明
设置配置文件类型和位置
1 2
| viper.SetConfigType("env") viper.AddConfigPath(".")
|
这两行不难理解,配置文件类型为env
,路径为当前目录.(相对于main.go)
自动读取环境变量
1 2
| viper.SetEnvPrefix("appenv") viper.AutomaticEnv()
|
对于程序员来说,我想配置环境变量并不陌生,借助于goland工具,我们可以快速配置环境变量,我们以此来举个例子
可以这样测试
main.go
1
| fmt.Println(config.Get("id"))
|
我们就可以看到打印值是1
ConfigFunc
1 2 3
| type ConfigFunc func() map[string]interface{}
var ConfigFuncs map[string]ConfigFunc
|
这里比较考验Go的基础,看不懂的去回顾一下type
的用法
初始化配置(读取配置)
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
| func InitConfig(env string) { loadEnv(env) loadConfig() }
func loadConfig() { for name, fn := range ConfigFuncs { viper.Set(name, fn()) } }
func loadEnv(envSuffix string) { envPath := ".env" if len(envSuffix) > 0 { filePath := envPath + envSuffix if _, err := os.Stat(filePath); err != nil { envPath = filePath } }
viper.SetConfigName(envPath) if err := viper.ReadInConfig(); err != nil { panic(err) }
viper.WatchConfig() }
|
主要讲一下loadEnv
,这里的后缀可以让我们根据运行环境的不同读取不同的配置文件,默认情况下是.env
,通过后缀我们可以读取.env.test
,.env.prod
,.env.dev
等
此外,如果你注意到了前面的viper.New()
,你就会发现下面的配置
也就是说我们必须要手动设置一次configName
1
| viper.SetConfigName(envPath)
|
否则默认的就是config.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 31 32 33 34 35 36 37 38 39
| func Env(envName string, defaultValue ...interface{}) interface{} { if len(defaultValue) > 0 { return internalGet(envName, defaultValue[0]) } return internalGet(envName) }
func Add(name string, configFn ConfigFunc) { ConfigFuncs[name] = configFn }
func Get(path string, defaultValue ...interface{}) string { return GetString(path, defaultValue...) }
func internalGet(path string, defaultValue ...interface{}) interface{} { if !viper.IsSet(path) || helpers.Empty(viper.Get(path)) { if len(defaultValue) > 0 { return defaultValue[0] } return nil } return viper.Get(path) }
func GetString(path string, defaultValue ...interface{}) string { return cast.ToString(internalGet(path, defaultValue...)) } func GetInt(path string, defaultValue ...interface{}) int { return cast.ToInt(internalGet(path, defaultValue...)) } func GetBool(path string, defaultValue ...interface{}) bool { return cast.ToBool(internalGet(path, defaultValue...)) }
|
这里的Env
和其它函数我都会在使用的时候统一讲解,现在留个印象即可,现在我们已经完成了viper工具类,下面我们完成helpers
的小插曲,把最重要的config
内容留到最后
helpers工具包
helpers/helpers.go
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 helpers
import "reflect"
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: 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()) }
|
因为viper.Get()
返回的是一个interface{}
,所以我们特别的来处理一下它的判空
其中reflect.DeepEqual()
就可以完全完成这个工作了
1
| reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface())
|
但是由于其中用了很多反射操作,速度比较慢,所以我们尽可能地处理一些自己可以处理的空类型判断,来加快程序的运行速度
完成config包和对配置过程加载的全解析
先贴上全部的代码,要注意,之前的config
工具包是在pkg
下的,现在我们要在根目录下新建一个config
包
config/app.go
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
| package config
import "go-api-practice/pkg/config"
func init() { config.Add("app", func() map[string]interface{} { return map[string]interface{}{
"name": config.Env("APP_NAME", "go-api-pratice"),
"env": config.Env("APP_ENV", "production"),
"debug": config.Env("APP_DEBUG", false),
"port": config.Env("APP_PORT", "3000"),
"key": config.Env("APP_KEY", "33446a9dcf9ea060a0a6532b166da32f304af0de"),
"url": config.Env("APP_URL", "http://localhost:3000"),
"timezone": config.Env("TIMEZONE", "Asia/Shanghai"), } }) }
|
config/config.go
1 2 3 4 5
| package config
func Initialize() { }
|
.env
1 2 3 4 5 6
| APP_ENV=local APP_KEY=zBqYyQrPNaIUsnRhsGtHLivjqiMjBVLS APP_DEBUG=true APP_URL=http://localhost:3000 APP_LOG_LEVEL=debug APP_PORT=3000
|
这里的配置文件名就叫做.env
加载过程分析
我们会从config.Add()
函数开始,按照函数执行步骤做一步一步的分析,函数细节请自己翻阅上面的代码,可能有点绕,请静下心来慢慢看
config.Add()
这里我们添加一个映射,从"app"
到一个func() map[string]interface{}
函数
loadEnv()
1 2 3 4
| viper.SetConfigName(envPath) if err := viper.ReadInConfig(); err != nil { panic(err) }
|
这一步viper读取了.env(或者别的环境)
文件,并且把这些键值对都加载到了viper中,形式如下
loadConfig()
1 2 3 4 5
| func loadConfig() { for name, fn := range ConfigFuncs { viper.Set(name, fn()) } }
|
这里我们去调用所有的ConfigFuncs
函数来设置键值对,这里的键目前只有"app"
,目前实际的内容是这样的
1 2 3 4
| app: name:XXX env:XXX ...
|
app
下面的所有内容都是fn()
的返回值,我们来分析一下这个函数的返回内容
1 2 3 4 5 6 7 8 9 10 11 12 13
| config.Add("app", func() map[string]interface{} { return map[string]interface{}{ "port": config.Env("APP_PORT", "3000"), "timezone": config.Env("TIMEZONE", "Asia/Shanghai"), "env": config.Env("APP_ENV", "production"), . . . } })
|
我们就以这个env
为例
config.Env()
1 2 3 4 5 6
| func Env(envName string, defaultValue ...interface{}) interface{} { if len(defaultValue) > 0 { return internalGet(envName, defaultValue[0]) } return internalGet(envName) }
|
没什么可说的,根据情况调用internalGet()
internalGet()
1 2 3 4 5 6 7 8 9
| func internalGet(path string, defaultValue ...interface{}) interface{} { if !viper.IsSet(path) || helpers.Empty(viper.Get(path)) { if len(defaultValue) > 0 { return defaultValue[0] } return nil } return viper.Get(path) }
|
这部分是关键,首先,方法会判断这个键是否存在,那么此时viper内部已经读入了什么呢,
没错,就是配置文件
这个时候的path
是APP_ENV(在Get时都会统一转化成小写)
,所以这个时候键存在,调用viper.Get(path)
,值为local
,所以返回值是local
,那么这样一个配置就确定下来了,下面的逻辑都是相同的
1 2 3 4
| app: name:XXX env:local ...
|
反之,加入我们在配置文件中如果没有app_env=local
,我们会发现我们调用函数的时候传入了一个默认值production
,所以,加入我们没有配置这一项,返回值就是production
1 2 3 4
| app: name:XXX env:production ...
|
这就是全部的加载过程了,如果你了解viper,就会知道viper.Set()
的优先级高于配置文件,但是我们最后发现在这个转换中,配置文件都被加载到了viper.Set()
中
使用配置
main.go
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 ( "flag" "fmt" "github.com/gin-gonic/gin" "go-api-practice/bootstrap" btsconfig "go-api-practice/config" "go-api-practice/pkg/config" )
func init() { btsconfig.Initialize() }
func main() { var env string flag.StringVar(&env, "env", "", "") flag.Parse() config.InitConfig(env)
router := gin.New()
bootstrap.SetupRoute(router)
err := router.Run(":" + config.Get("app.port")) if err != nil { fmt.Println(err) } }
|
到这里我相信你依然可能会有几个疑惑的点,我们来一一解答
btsconfig.Initialize()的作用
我们知道Go中的init()
函数会在main
函数之前被调用,而对于别的包的init()
函数而言,它们被调用的时候就是这个包被引用的时候,如果你细心的话就会发现app.go
的内容是写在init()
函数中的,所以btsconfig.Initialize()
的作用就是调用config
包下的所有init()
函数
关于flag包
作用是读取命令行参数,这里就简单的说明一下作用
1
| $ go run main.go --env .dev
|
这样我们在运行的时候就可以读取.env.dev
配置文件啦(结合InitConfig函数)
关于app.port
还记得最前面的viper初始化时的viper.New()
吗
对于配置的多层嵌套,viper自有它的读取方法,我们使用viper.Get()
时中间用.
隔开
到这里,viper集成就完成了,这部分内容有些复杂,慢慢来,你可以休息一下,好好总结上面的内容然后再开启下一节。