配置解决方案——viper框架
文件读取配置
package configimport ("github.com/spf13/viper"_ "github.com/spf13/viper/remote""io""log"
)// 从本地文件读取配置
func LoadFromFile(filepath string, typ ...string) (*viper.Viper, error) {v := viper.New()v.SetConfigFile(filepath)if len(typ) > 0 {v.SetConfigType(typ[0])}err := v.ReadInConfig()return v, err
}
环境变量读取配置
package configimport ("github.com/spf13/viper"_ "github.com/spf13/viper/remote""io""log"
)func LoadFromEnv() (*viper.Viper, error) {v := viper.New()//自动绑定环境变量//v.AutomaticEnv()//指定绑定的环境变量v.BindEnv("GOPATH")v.BindEnv("GOROOT")return v, nil
}
io.Reader读取
package configimport ("github.com/spf13/viper"_ "github.com/spf13/viper/remote""io""log"
)func LoadFromIoReader(reader io.Reader, typ string) (*viper.Viper, error) {v := viper.New()v.SetConfigType(typ)err := v.ReadConfig(reader)return v, err
}
etcd读取
etcd单节点集群运行
docker run -d \
-p 2379:2379 \
-p 2380:2380 \
--restart always \
--privileged \
--volume=/home/etcd:/etcd-data \
--name etcd quay.io/coreos/etcd:v3.5.7 \
/usr/local/bin/etcd \
--data-dir=/etcd-data --name node1 \
--initial-advertise-peer-urls http://10.74.18.61:2380 \
--listen-peer-urls http://0.0.0.0:2380 \
--advertise-client-urls http://10.74.18.61:2379 \
--listen-client-urls http://0.0.0.0:2379 \
--initial-cluster node1=http://10.74.18.61:2380 \
--initial-cluster-token tkn \
--initial-cluster-state new
写入etcd配置文件, 读取etcd 配置文件
viper 读取etcd 配置
config.go
package configimport ("github.com/spf13/viper"_ "github.com/spf13/viper/remote""io""log"
)
func LoadFromEtcd(etcdAddr, key, typ string) (*viper.Viper, error) {v := viper.New()// viper读取etcd中的一个keyerr := v.AddRemoteProvider("etcd3", etcdAddr, key)if err != nil {log.Println(err)return nil, err}v.SetConfigType(typ)err = v.ReadRemoteConfig()return v, err
}
package mainimport ("bytes""context""fmt"clientv3 "go.etcd.io/etcd/client/v3""log""os"_ "os/signal""viper-practice/config"
)func loadEtcd() {//向etcd写入数据etcdAddr := "10.74.18.61:2379"key := "/test/viper/config.yaml"localFilepath := "config.yaml"writeConfToEtcd(etcdAddr, key, localFilepath)//从etcd加载配置,etcd充当了配置中心v, err := config.LoadFromEtcd(etcdAddr, key, "yaml")fmt.Println("etcd", err, v.Get("env"), v.Get("server.port"), v.Get("courses").([]interface{})[0], v.Get("list").([]interface{})[0].(map[string]interface{})["author"])
}
func writeConfToEtcd(etcdAddr, key, localFilepath string) {byteList, err := os.ReadFile(localFilepath)if err != nil {log.Fatal(err)}value := string(byteList)cli, err := clientv3.New(clientv3.Config{Endpoints: []string{etcdAddr},})if err != nil {log.Fatal(err)}_, err = cli.Put(context.Background(), key, value)if err != nil {log.Fatal(err)}
}func main() {//loadFile()//loadEnv()//loadReader()loadEtcd()
}
docker etcd查询
docker exec etcd etcdctl get [key]
汇总
source.go
package configimport ("github.com/spf13/viper"_ "github.com/spf13/viper/remote""io""log"
)// LoadFromFile 从本地文件读取配置
func LoadFromFile(filepath string, typ ...string) (*viper.Viper, error) {v := viper.New()v.SetConfigFile(filepath)if len(typ) > 0 {v.SetConfigType(typ[0])}err := v.ReadInConfig()return v, err
}func LoadFromEnv() (*viper.Viper, error) {v := viper.New()//自动绑定环境变量//v.AutomaticEnv()//指定绑定的环境变量v.BindEnv("GOPATH")v.BindEnv("GOROOT")return v, nil
}func LoadFromIoReader(reader io.Reader, typ string) (*viper.Viper, error) {v := viper.New()v.SetConfigType(typ)err := v.ReadConfig(reader)return v, err
}func LoadFromEtcd(etcdAddr, key, typ string) (*viper.Viper, error) {v := viper.New()// viper读取etcd中的一个keyerr := v.AddRemoteProvider("etcd3", etcdAddr, key)if err != nil {log.Println(err)return nil, err}v.SetConfigType(typ)err = v.ReadRemoteConfig()return v, err
}
测试
main.go
package mainimport ("bytes""context""fmt"clientv3 "go.etcd.io/etcd/client/v3""log""os"_ "os/signal""viper-practice/config"
)func main() {//loadFile()//loadEnv()//loadReader()loadEtcd()
}func loadFile() {// 获取viper对象,打印对应配置// viper对象里以键值对方式存储// 环境变量的值都是字符串处理,没有复杂的对象存储v1, err := config.LoadFromFile("config.env")fmt.Println("config.env", err, v1.Get("env"), v1.Get("server.port"), v1.Get("courses"))v2, err := config.LoadFromFile("config.json")fmt.Println("config.json", err, v2.Get("env"), v2.Get("server.port"), v2.Get("courses").([]interface{})[0], v2.Get("list").([]interface{})[0].(map[string]interface{})["author"])// 识别不了的格式,可以指定格式读取v3, err := config.LoadFromFile("config.noext", "yaml")fmt.Println("config.noext", err, v3.Get("env"), v3.Get("server.port"), v3.Get("courses").([]interface{})[0], v3.Get("list").([]interface{})[0].(map[string]interface{})["author"])v4, err := config.LoadFromFile("config.toml")fmt.Println("config.toml", err, v4.Get("env"), v4.Get("server.port"), v4.Get("courses").(map[string]interface{})["list"].([]interface{})[0], v4.Get("list").([]interface{})[0].(map[string]interface{})["author"])v5, err := config.LoadFromFile("config.yaml")fmt.Println("config.yaml", err, v5.Get("env"), v5.Get("server.port"), v5.Get("courses").([]interface{})[0], v5.Get("list").([]interface{})[0].(map[string]interface{})["author"])// 这种只是测试一下viper, 不推荐这么干, 一般是定义结构去读取配置文件比较方便
}
func loadEnv() {v, err := config.LoadFromEnv()fmt.Println(err, v.Get("GOROOT"), v.Get("GOPATH"))
}
func loadReader() {byteList, err := os.ReadFile("config.yaml")if err != nil {log.Fatal(err)}r := bytes.NewReader(byteList)v, err := config.LoadFromIoReader(r, "yaml")fmt.Println("io.reader", err, v.Get("env"), v.Get("server.port"), v.Get("courses").([]interface{})[0], v.Get("list").([]interface{})[0].(map[string]interface{})["author"])}func loadEtcd() {//向etcd写入数据etcdAddr := "10.74.18.61:2379"key := "/test/viper/config.yaml"localFilepath := "config.yaml"writeConfToEtcd(etcdAddr, key, localFilepath)//从etcd加载配置,etcd充当了配置中心v, err := config.LoadFromEtcd(etcdAddr, key, "yaml")fmt.Println("etcd", err, v.Get("env"), v.Get("server.port"), v.Get("courses").([]interface{})[0], v.Get("list").([]interface{})[0].(map[string]interface{})["author"])
}
func writeConfToEtcd(etcdAddr, key, localFilepath string) {byteList, err := os.ReadFile(localFilepath)if err != nil {log.Fatal(err)}value := string(byteList)cli, err := clientv3.New(clientv3.Config{Endpoints: []string{etcdAddr},})if err != nil {log.Fatal(err)}_, err = cli.Put(context.Background(), key, value)if err != nil {log.Fatal(err)}
}
配置映射到struct以及配置热更新
生产工作中,我们还是采用配置映射到struct的方式。同时在配置变化的时候,希望可以做到配置的热更新,不用重新启动程序
下面是一个配置文件结构定义,viper有预定义的标签tag mapstructure
来标识配置项
监控本地文件
package configimport ("fmt""github.com/fsnotify/fsnotify""github.com/spf13/viper""log""time"
)
// mapstructure 是viper特有的tag
type Config struct {Env string `mapstructure:"env"`Server struct {IP string `mapstructure:"ip"`Port string `mapstructure:"port"`}Courses []string `mapstructure:"courses"`List []struct {Name string `mapstructure:"name"`Author string `mapstructure:"author"`} `mapstructure:"list"`
}var conf *Config
var etcdConf *Configfunc InitConf(filepath string, typ ...string) {v := viper.New()v.SetConfigFile(filepath)if len(typ) > 0 {v.SetConfigType(typ[0])}err := v.ReadInConfig()if err != nil {log.Fatal(err)}conf = &Config{}err = v.Unmarshal(conf)if err != nil {log.Fatal(err)}v.OnConfigChange(func(in fsnotify.Event) {// 重新加载配置文件到对象v.Unmarshal(conf)// 打印新的配置信息,验证结果fmt.Printf("%+v\n", conf)})v.WatchConfig()
}
监控etcd配置热更
如何将配置写入etcd在前面已经提到,生产工作中一般有三方的程序会做这个事情。
// etcd配置热更新
func InitEtcdConf(etcdAddr, key, typ string) {v := viper.New()v.AddRemoteProvider("etcd3", etcdAddr, key)v.SetConfigType(typ)err := v.ReadRemoteConfig()if err != nil {log.Fatal(err)}etcdConf = &Config{}err = v.Unmarshal(etcdConf)if err != nil {log.Fatal(err)}go func() {i := 0for {<-time.After(time.Second * 3)err = v.WatchRemoteConfig()if err != nil {log.Println(err)continue}etcdConf = &Config{}err = v.Unmarshal(etcdConf)if err != nil {log.Println(err)continue}i++fmt.Printf("%d, %+v\n", i, etcdConf)}}()
}