开始前的准备
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集成就完成了,这部分内容有些复杂,慢慢来,你可以休息一下,好好总结上面的内容然后再开启下一节。