Go项目学习(3)-cobra+viper仿SpringBoot读取配置

发布时间 2023-09-22 14:17:10作者: 季夏三

实现目标

  1. 读取yaml配置文件,并解析到实体
  2. 读取命令行flag,覆盖yaml配置文件内容

思路

针对第一点,viper已经实现这个功能,只需要监听配置文件的变化,重新赋值给实体对象即可。配置文件默认目录设置两个:../configs,也可以通过命令flag指定。

针对第二点,viper实现了将flag读取到配置,并且优先级比配置文件高,现在需要做的是解析实体对象字段,动态添加flag。

计划搭建一个web服务,先只创建一个serve命令,给serve命令添加一系列flag

实践

  1. 使用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
  1. 配置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
  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
  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,一步一步慢慢学,正所谓:不积跬步,无以至千里。