【Viper】文件、Etcd应用配置与配置热更新,go案例

1. 应用配置

Viper 的核心功能之一是能够将配置应用到 Go 应用程序中。它支持从多种数据源加载配置,并将这些配置绑定到程序中的变量或结构体

2. 配置热更新

在某些应用场景下,我们希望 修改配置文件后,应用能够自动更新配置,而不需要重启程序。这在微服务、长时间运行的应用程序中非常有用。


1. 从文件加载配置,配置热更新

步骤 代码示例 说明
1. 设置配置文件 viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") 设置配置文件名、类型和搜索路径。
2. 读取配置文件 if err := viper.ReadInConfig(); err != nil { ... } 读取配置文件,如果文件不存在或格式错误会返回错误。
3. 绑定配置到变量 dbHost := viper.GetString("database.host") 将配置值绑定到变量。
4. 绑定配置到结构体 var config Config if err := viper.Unmarshal(&config); err != nil { ... } 将配置绑定到结构体,使用 mapstructure 标签映射字段。
5. 启用文件监听 viper.WatchConfig() 启用文件监听功能,支持热更新。
6. 注册回调函数 viper.OnConfigChange(func(e fsnotify.Event) { ... }) 配置文件变化时触发回调函数。

2. 从 etcd 加载配置,配置热更新

步骤 代码示例 说明
1. 添加远程提供者 viper.AddRemoteProvider("etcd", "http://127.0.0.1:2379", "/config/path") 设置 etcd 的地址和配置路径。
2. 设置配置类型 viper.SetConfigType("yaml") 设置远程配置的类型(如 yaml、json 等)。
3. 读取远程配置 if err := viper.ReadRemoteConfig(); err != nil { ... } 从 etcd 读取配置。
4. 绑定配置到变量 dbHost := viper.GetString("database.host") 将配置值绑定到变量。
5. 绑定配置到结构体 var config Config if err := viper.Unmarshal(&config); err != nil { ... } 将配置绑定到结构体,使用 mapstructure 标签映射字段。
6. 启用远程监听 viper.WatchRemoteConfig() 启用远程配置监听功能,支持热更新。
7. 注册回调函数 viper.OnRemoteConfigChange(func() { ... }) 远程配置变化时触发回调函数。

3. 应用配置

步骤 代码示例 说明
1. 绑定配置到变量 dbHost := viper.GetString("database.host") 直接通过键获取配置值。
2. 绑定配置到结构体 var config Config if err := viper.Unmarshal(&config); err != nil { ... } 将配置绑定到结构体,适合复杂配置。

4. 配置热更新

步骤 代码示例 说明
1. 文件监听热更新 viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { ... }) 监听本地配置文件变化,触发回调函数。
2. 远程监听热更新 viper.WatchRemoteConfig() viper.OnRemoteConfigChange(func() { ... }) 监听远程配置(如 etcd)变化,触发回调函数。

  • 从文件应用
go 复制代码
// InitConf 初始化配置函数,从指定文件路径加载配置并解析到全局变量 conf 中。
// 参数:
//   - filepath: 配置文件的路径
//   - typ: 可选参数,指定配置文件的类型(如 "yaml", "json" 等)
//
// 返回值: 无
func InitConf(filepath string, typ ...string) {
	// 创建 Viper 实例用于读取配置文件
	v := viper.New()
	v.SetConfigFile(filepath)

	// 如果指定了配置文件类型,则设置类型
	if len(typ) > 0 {
		v.SetConfigType(typ[0])
	}

	// 读取配置文件内容
	err := v.ReadInConfig()
	if err != nil {
		log.Fatalln(err) // 如果读取失败,记录错误并终止程序
	}

	// 初始化全局配置变量
	conf = &Config{}
	// 将配置文件内容解析到 conf 结构体中
	err = v.Unmarshal(conf)
	if err != nil {
		log.Fatalln(err) // 如果解析失败,记录错误并终止程序
	}

	// OnConfigChange 注册一个回调函数,当配置文件发生变化时触发。
	// 该回调函数接收一个 fsnotify.Event 类型的参数 in,表示文件系统事件。
	// 在回调函数中,使用 viper 的 Unmarshal 方法将配置文件内容解析到 conf 结构体中,
	// 并打印出 conf 的内容。
	v.OnConfigChange(func(in fsnotify.Event) {
		// 这里实践发现更改一次文件总是会连续触发两次,所以加个时间间隔
		if time.Since(lastEventTime) < time.Second {
			return
		}
		lastEventTime = time.Now()

		v.Unmarshal(conf)
		fmt.Printf("%+v\n", conf)
		fmt.Println("配置文件发生变化", in.String())
	})

	// WatchConfig 启动 viper 的配置文件监视功能,当配置文件发生变化时,
	// 会触发之前注册的 OnConfigChange 回调函数。
	v.WatchConfig() // 观察到更改,触发OnConfigChange
}

  • 从Etcd应用
go 复制代码
// InitEtcdConf 初始化并监控从 etcd 中获取的配置。
// 该函数通过 etcd 地址、配置键和配置类型来获取远程配置,并将其解析到全局变量 etcdConf 中。
// 同时,该函数会启动一个后台 goroutine,每隔 2 秒检查一次配置是否有更新,并在配置更新时重新解析并打印配置。
//
// 参数:
//   - etcdAddr: etcd 服务器的地址。
//   - key: 在 etcd 中存储配置的键。
//   - typ: 配置的类型(如 "json", "yaml" 等)。
func InitEtcdConf(etcdAddr, key, typ string) {
	// 创建一个新的 Viper 实例,用于管理远程配置
	v := viper.New()
	// 添加 etcd3 作为远程配置提供者
	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)
	}

	// 启动一个后台 goroutine 监控配置的更新
	go func() {
		i := 0
		for {
			// 每隔 2 秒检查一次配置更新
			<-time.After(time.Second * 2)
			// 监控远程配置的更新
			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)
		}
	}()
}

main.go

go 复制代码
// 从文件初始化配置,加载配置文件,并打印部分配置项,热更新
	config.InitConf("config.yaml")
	conf := config.GetConf()
	fmt.Printf("%+v\n", conf)
	fmt.Println(conf.Server.IP)

	// 定义 etcd 的地址、配置项的键以及本地配置文件的路径
	etcdAddr := "192.168.88.131:2379"
	key := "/0voice/viper/config.json"
	loaclFilepath := "config.json"
	writeConfToEtcd(etcdAddr, key, loaclFilepath)

	config.InitEtcdConf(etcdAddr, key, "json")
	conf = config.GetEtcdConf()
	fmt.Printf("%+v\n", conf)
	fmt.Println(conf.Server.IP)

完整go案例

config.go

go 复制代码
package config

import (
	"fmt"
	"github.com/fsnotify/fsnotify"
	"github.com/spf13/viper"
	"log"
	"time"
)

// Config 结构体用于存储配置信息,包含环境变量、服务器信息、课程列表以及其他列表项。
type Config struct {
	Env    string `mapstructure:"env"` // 环境变量,如 "dev" 或 "prod"
	Server struct {
		IP   string `mapstructure:"ip"`   // 服务器IP地址
		Port string `mapstructure:"port"` // 服务器端口号
	} `mapstructure:"server"` // 服务器相关配置
	Courses []string `mapstructure:"courses"` // 课程列表
	List    []struct {
		Name   string `mapstructure:"name"`   // 列表项名称
		Author string `mapstructure:"author"` // 列表项作者
	} `mapstructure:"list"` // 其他列表项
}

var conf *Config     // 全局配置变量
var etcdConf *Config // ETCD配置变量
var lastEventTime time.Time

// InitConf 初始化配置函数,从指定文件路径加载配置并解析到全局变量 conf 中。
// 参数:
//   - filepath: 配置文件的路径
//   - typ: 可选参数,指定配置文件的类型(如 "yaml", "json" 等)
//
// 返回值: 无
func InitConf(filepath string, typ ...string) {
	// 创建 Viper 实例用于读取配置文件
	v := viper.New()
	v.SetConfigFile(filepath)

	// 如果指定了配置文件类型,则设置类型
	if len(typ) > 0 {
		v.SetConfigType(typ[0])
	}

	// 读取配置文件内容
	err := v.ReadInConfig()
	if err != nil {
		log.Fatalln(err) // 如果读取失败,记录错误并终止程序
	}

	// 初始化全局配置变量
	conf = &Config{}
	// 将配置文件内容解析到 conf 结构体中
	err = v.Unmarshal(conf)
	if err != nil {
		log.Fatalln(err) // 如果解析失败,记录错误并终止程序
	}

	// OnConfigChange 注册一个回调函数,当配置文件发生变化时触发。
	// 该回调函数接收一个 fsnotify.Event 类型的参数 in,表示文件系统事件。
	// 在回调函数中,使用 viper 的 Unmarshal 方法将配置文件内容解析到 conf 结构体中,
	// 并打印出 conf 的内容。
	v.OnConfigChange(func(in fsnotify.Event) {
		// 这里实践发现更改一次文件总是会连续触发两次,所以加个时间间隔
		if time.Since(lastEventTime) < time.Second {
			return
		}
		lastEventTime = time.Now()

		v.Unmarshal(conf)
		fmt.Printf("%+v\n", conf)
		fmt.Println("配置文件发生变化", in.String())
	})

	// WatchConfig 启动 viper 的配置文件监视功能,当配置文件发生变化时,
	// 会触发之前注册的 OnConfigChange 回调函数。
	v.WatchConfig() // 观察到更改,触发OnConfigChange
}

func GetConf() *Config {
	return conf
}

// InitEtcdConf 初始化并监控从 etcd 中获取的配置。
// 该函数通过 etcd 地址、配置键和配置类型来获取远程配置,并将其解析到全局变量 etcdConf 中。
// 同时,该函数会启动一个后台 goroutine,每隔 2 秒检查一次配置是否有更新,并在配置更新时重新解析并打印配置。
//
// 参数:
//   - etcdAddr: etcd 服务器的地址。
//   - key: 在 etcd 中存储配置的键。
//   - typ: 配置的类型(如 "json", "yaml" 等)。
func InitEtcdConf(etcdAddr, key, typ string) {
	// 创建一个新的 Viper 实例,用于管理远程配置
	v := viper.New()
	// 添加 etcd3 作为远程配置提供者
	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)
	}

	// 启动一个后台 goroutine 监控配置的更新
	go func() {
		i := 0
		for {
			// 每隔 2 秒检查一次配置更新
			<-time.After(time.Second * 2)
			// 监控远程配置的更新
			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)
		}
	}()
}

func GetEtcdConf() *Config {
	return etcdConf
}

main.go

go 复制代码
package main

import (
	"bytes"
	"context"
	"fmt"
	clientv3 "go.etcd.io/etcd/client/v3"
	"golang20-viper/config"
	"log"
	"os"
	"os/signal"
)

func main() {
	//loadFile()
	//loadEnv()
	//loadReader()
	//loadEtcd()

	// 从文件初始化配置,加载配置文件,并打印部分配置项,热更新
	//config.InitConf("config.yaml")
	//conf := config.GetConf()
	//fmt.Printf("%+v\n", conf)
	//fmt.Println(conf.Server.IP)

	// 定义 etcd 的地址、配置项的键以及本地配置文件的路径
	etcdAddr := "192.168.88.131:2379"
	key := "/0voice/viper/config.json"
	loaclFilepath := "config.json"
	writeConfToEtcd(etcdAddr, key, loaclFilepath)

	config.InitEtcdConf(etcdAddr, key, "json")
	conf := config.GetEtcdConf()
	fmt.Printf("%+v\n", conf)
	fmt.Println(conf.Server.IP)

	// 创建一个上下文(context)对象,并将其与信号通知机制绑定。
	// 当接收到 os.Interrupt 或 os.Kill 信号时,上下文将被取消。
	// 使用 defer 确保在函数退出时停止信号监听,释放资源。
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
	defer stop()

	// 等待上下文被取消,通常是在接收到信号后。
	// 这行代码会阻塞,直到 ctx.Done() 通道被关闭。
	<-ctx.Done()
}

// loadFile 函数演示了如何从不同格式的配置文件中加载配置。
// 它依次尝试加载 .env, .json, .yaml, .toml 和 .yaml 文件,并打印出特定配置项的值。
func loadFile() {
	// 从 "config.env" 文件加载配置,并打印出环境、服务器端口和课程信息。
	v1, err := config.LoadFromFile("config.env", "env")
	fmt.Println("config.env", err, v1.Get("env"), v1.Get("server.port"), v1.Get("courses"))

	// 从 "config.json" 文件加载配置,包括环境、服务器端口、课程信息和作者信息。
	v2, err := config.LoadFromFile("config.json")
	fmt.Println("config.json", err, v2.Get("env"), v2.Get("server.port"), v2.Get("courses").([]any)[0], v2.Get("list").([]any)[0].(map[string]any)["author"])

	// 从 "config.noext" 文件加载 YAML 格式的配置,同样打印环境、服务器端口、课程信息和作者信息。
	v3, err := config.LoadFromFile("config.noext", "yaml")
	fmt.Println("config.noext", err, v3.Get("env"), v3.Get("server.port"), v3.Get("courses").([]any)[0], v3.Get("list").([]any)[0].(map[string]any)["author"])

	// 从 "config.toml" 文件加载配置,展示如何获取嵌套配置项的值。
	v4, err := config.LoadFromFile("config.toml")
	fmt.Println("config.toml", err, v4.Get("env"), v4.Get("server.port"), v4.Get("courses").(map[string]any)["list"].([]any)[0], v4.Get("list").([]any)[0].(map[string]any)["author"])

	// 从 "config.yaml" 文件加载配置,再次打印出环境、服务器端口、课程信息和作者信息,验证不同格式文件的兼容性。
	v5, err := config.LoadFromFile("config.yaml")
	fmt.Println("config.yaml", err, v5.Get("env"), v5.Get("server.port"), v5.Get("courses").([]any)[0], v5.Get("list").([]any)[0].(map[string]any)["author"])
}

func loadEnv() {
	v, err := config.LoadFromEnv()
	fmt.Println(err, v.Get("GOROOT"), v.Get("gopath"))
}

// loadReader 读取配置文件并解析特定配置项。
// 该函数没有输入参数和返回值。
// 功能描述:
// 1. 读取名为 "config.yaml" 的配置文件。
// 2. 如果读取过程中遇到错误,记录错误信息并终止程序运行。
// 3. 使用 bytes.NewReader 创建一个字节流读取器来读取配置文件内容。
// 4. 调用 config.LoadFromIoReader 函数从字节流读取器中加载配置信息。
// 5. 打印解析后的配置项,包括环境变量、服务器端口、课程信息和作者信息。
func loadReader() {
	// 读取配置文件 "config.yaml" 的内容到 byteList。
	byteList, err := os.ReadFile("config.yaml")
	if err != nil {
		// 如果读取配置文件时发生错误,记录错误信息并终止程序。
		log.Fatalln(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").([]any)[0], v.Get("list").([]any)[0].(map[string]any)["author"])
}

// loadEtcd 函数用于从本地文件加载配置并写入到 etcd 中,然后从 etcd 中读取配置并打印部分配置项。
// 该函数不接收任何参数,也不返回任何值。
func loadEtcd() {
	// 定义 etcd 的地址、配置项的键以及本地配置文件的路径
	etcdAddr := "192.168.88.131:2379"
	key := "/0voice/viper/config.yaml"
	loaclFilepath := "config.yaml"

	// 将本地配置文件的内容写入到 etcd 中
	writeConfToEtcd(etcdAddr, key, loaclFilepath)

	// 从 etcd 中加载配置,并解析为 yaml 格式
	v, err := config.LoadFromEtcd(etcdAddr, key, "yaml")

	// 打印从 etcd 中读取的配置项,包括环境、服务器端口、课程列表中的第一个课程以及列表中的第一个作者的名称
	fmt.Println("etcd", err, v.Get("env"), v.Get("server.port"), v.Get("courses").([]any)[0], v.Get("list").([]any)[0].(map[string]any)["author"])
	//fmt.Println(err, v)
}

// writeConfToEtcd 将本地配置文件内容写入到etcd指定键中
// 参数说明:
//   - etcdAddr: etcd服务地址,格式为"IP:PORT"
//   - key: etcd中存储配置的键名
//   - localFilepath: 本地配置文件的路径
//
// 功能说明:
//
//	读取配置文件,将其内容存入etcd集群的指定键中
//	遇到任何错误(文件读取、连接etcd、写入etcd)将直接终止程序
func writeConfToEtcd(etcdAddr, key, localFilepath string) {
	// 从固定路径读取配置文件内容
	byteList, err := os.ReadFile(localFilepath)
	if err != nil {
		// 如果读取配置文件时发生错误,记录错误信息并终止程序。
		log.Fatalln(err)
	}
	v := string(byteList)

	// 创建etcd客户端连接
	cli, err := clientv3.New(clientv3.Config{
		Endpoints: []string{etcdAddr},
	})
	if err != nil {
		log.Fatal(err)
	}

	// 将配置内容写入etcd指定键
	_, err = cli.Put(context.Background(), key, v)
	if err != nil {
		log.Fatal(err)
	}
}

https://github.com/0voice

相关推荐
多则惑少则明5 分钟前
java 代码查重(三)常见的距离算法和相似度(相关系数)计算方法
java·算法·常见的距离算法和相似度
头发那是一根不剩了1 小时前
Spring Boot 注解 @ConditionalOnMissingBean是什么
java·spring boot·后端
天若有情6732 小时前
探秘 C++ 计数器类:从基础实现到高级应用
java·开发语言·c++
明天不下雨(牛客同名)2 小时前
介绍一下 MVCC
java·服务器·数据库
春生野草2 小时前
如何用JAVA手写一个Tomcat
java·开发语言·tomcat
小猪咪piggy3 小时前
【JavaEE】(1) 计算机如何工作
java
BUG制造机.3 小时前
代码走读 Go 语言 Map 的实现
golang·哈希算法·散列表
smileNicky3 小时前
SpringBoot系列之OpenAI API 创建智能博客评论助手
java·spring boot·后端
弥鸿3 小时前
MinIO的安装和使用
java·spring boot·java-ee·springcloud·javaee