实现目标
- 读取yaml配置文件,并解析到实体
- 读取命令行flag,覆盖yaml配置文件内容
思路
针对第一点,viper已经实现这个功能,只需要监听配置文件的变化,重新赋值给实体对象即可。配置文件默认目录设置两个:.
、./configs
,也可以通过命令flag指定。
针对第二点,viper实现了将flag读取到配置,并且优先级比配置文件高,现在需要做的是解析实体对象字段,动态添加flag。
计划搭建一个web服务,先只创建一个serve命令,给serve命令添加一系列flag
实践
- 使用cobra-cli搭建项目,新建serve命令
go mod init demo
cobra-cli init
cobra-cli add serve
go get github.com/spf13/viper
# 项目结构
.
├── LICENSE
├── cmd
│ ├── root.go
│ └── serve.go
├── go.mod
├── go.sum
└── main.go
- 配置viper,读取配置文件、读取环境变量。其中配置文件来自flag指定路径,来自默认路径,指定路径优先默认路径。
// cmd/serve.go
var (
cnfFile string
// serveCmd represents the serve command
serveCmd = &cobra.Command{
Use: "serve",
Short: "A brief description of your command",
Long: `A longer description`,
PreRun: func(cmd *cobra.Command, args []string) {
initConfig(cmd)
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("serve called")
},
}
)
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().StringVar(&cnfFile, "config", "", "config file path, default config.yaml in \".\" and \"./configs\"")
}
func initConfig(cmd *cobra.Command) {
viper.BindPFlags(cmd.Flags())
if cnfFile != "" {
viper.SetConfigFile(cnfFile)
} else {
viper.AddConfigPath(".")
viper.AddConfigPath("./configs")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
}
viper.WatchConfig()
err := viper.ReadInConfig()
if err != nil {
fmt.Println("error when read config file: ", err)
os.Exit(1)
}
fmt.Printf("using config file: %s\n", viper.ConfigFileUsed())
viper.AutomaticEnv()
}
新建配置文件configs/config.yaml
application:
name: go-easy
version: 0.0.1
- 配置实体对象,将配置存入实体。
新建文件common/config/entity.go
// common/config/entity.go
package config
var Configs Config
type Config struct {
Application Application `mapstructure:"application"`
}
type Application struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
}
修改initConfig()
// cmd/serve.go
func initConfig(cmd *cobra.Command) {
viper.BindPFlags(cmd.Flags())
if cnfFile != "" {
viper.SetConfigFile(cnfFile)
} else {
viper.AddConfigPath(".")
viper.AddConfigPath("./configs")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
}
viper.WatchConfig()
err := viper.ReadInConfig()
if err != nil {
fmt.Println("error when read config file: ", err)
os.Exit(1)
}
fmt.Printf("using config file: %s\n", viper.ConfigFileUsed())
viper.AutomaticEnv()
viper.Unmarshal(&config.Configs)
// when config file changed, should unmarshal again
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("config file changed, update.")
viper.Unmarshal(&config.Configs)
})
}
添加启动方法,打印配置
// cmd/serve.go
// ...
serveCmd = &cobra.Command{
Use: "serve",
Short: "start a web server",
Long: `start a web server`,
PreRun: func(cmd *cobra.Command, args []string) {
initConfig(cmd)
},
Run: func(cmd *cobra.Command, args []string) {
run()
},
}
// ...
func run() {
fmt.Println("server is running")
fmt.Println("welcome to ", config.Configs.Application.Name)
fmt.Println("version: ", config.Configs.Application.Version)
}
运行测试:go run . serve
# output
using config file: /path/to/your/project/configs/config.yaml
server is running
welcome to go-easy
version: 0.0.1
- 根据实体添加flag
添加文件common/config/config.go
// GetFlagMap returns a map, which key is config key, and value is Configs's fields' reflect.Value.
func GetFlagMap() map[string]reflect.Value {
m := make(map[string]reflect.Value)
err := parseFlagConfig("", "", Configs, &m)
if err != nil {
panic(err)
}
return m
}
func parseFlagConfig(parentKey string, name string, c interface{}, m *map[string]reflect.Value) error {
var key string
if parentKey != "" && name != "" {
key = parentKey + "." + name
} else {
key = parentKey + name
}
t := reflect.TypeOf(c)
v := reflect.ValueOf(c)
switch t.Kind() {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64,
reflect.String,
reflect.Array, reflect.Slice:
(*m)[key] = v
return nil
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fv := v.Field(i)
n := strings.ToLower(f.Name)
err := parseFlagConfig(key, n, fv.Interface(), m)
if err != nil {
return err
}
}
return nil
default:
return fmt.Errorf("error occurred while parsing flag config: unsupport type in struct")
}
}
递归遍历配置实体的属性,使用.
链接父子属性,放到map里。
接下来在命令里拿到这个map,依次添加flag。类型过多,有点粗糙。
// cmd/serve.go
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().StringVar(&cnfFile, "config", "", "config file path, default config.yaml in \".\" and \"./configs\"")
m := config.GetFlagMap()
usage := "overwrite config file"
for k, v := range m {
switch v.Type().Kind() {
case reflect.Bool:
serveCmd.Flags().Bool(k, v.Bool(), usage)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
serveCmd.Flags().Int64(k, v.Int(), usage)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
serveCmd.Flags().Uint64(k, v.Uint(), usage)
case reflect.Float32, reflect.Float64:
serveCmd.Flags().Float64(k, v.Float(), usage)
case reflect.String:
serveCmd.Flags().String(k, v.String(), usage)
case reflect.Slice:
et := v.Type().Elem()
switch et.Kind() {
case reflect.Bool:
serveCmd.Flags().BoolSlice(k, []bool{}, usage)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
serveCmd.Flags().Int64Slice(k, []int64{}, usage)
case reflect.Float32, reflect.Float64:
serveCmd.Flags().Float64Slice(k, []float64{}, usage)
case reflect.String:
serveCmd.Flags().StringSlice(k, []string{}, usage)
}
}
}
}
遍历map,根据属性的类型设置不同的flag,这里类型太多,因为懒,粗略合并起来了。
验证
go run . serve -h
# output
start a web server
Usage:
demo serve [flags]
Flags:
--application.name string overwrite config file
--application.version string overwrite config file
--config string config file path, default config.yaml in "." and "./configs"
-h, --help help for serve
go run . serve --application.name="hello world"
# output
using config file: /Users/xujiahao/code/Go/demo/configs/config.yaml
server is running
welcome to hello world
version: 0.0.1
至此告一段落。
后语
思路是这么个思路,目前还只是半成品,没有经过周密的测试,遇到问题再调整,也欢迎大佬们指点。
下一步会加入gin,一步一步慢慢学,正所谓:不积跬步,无以至千里。