服务计算 读简单ini配置文件
1. 任务介绍
1.1 项目地址
1.2 任务内容
在 Gitee 或 GitHub 上发布一个读配置文件程序包,第一版仅需要读 ini 配置,配置文件格式案例:
# possible values : production, development
app_mode = development
[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
data = /home/git/grafana
[server]
# Protocol (http or https)
protocol = http
# The http port to use
http_port = 9999
# Redirect to correct domain if host header does not match domain
# Prevents DNS rebinding attacks
enforce_domain = true
1.3 任务要求
- 核心任务:包必须提供一个函数
Watch(filename,listener) (configuration, error)
- 输入 filename 是配置文件名
- 输入 listener 一个特殊的接口,用来监听配置文件是否被修改,让开发者自己决定如何处理配置变化
type ListenFunc func(string)
type inteface Listener { listen(inifile string) }
ListenFunc
实现接口方法listen
直接调用函数
- 输出 configuration 数据类型,可根据 key 读对应的 value。 key 和 value 都是字符串
- 输出 error 是错误数据,如配置文件不存在,无法打开等
- 可选的函数
WatchWithOption(filename,listener,...) (configuration, error)
- 包必须包括以下内容:
- 生成的中文 api 文档
- 有较好的 Readme 文件,包括一个简单的使用案例
- 每个go文件必须有对应的测试文件
- 必须提供自定义错误
- 使用
init
函数,使得 Unix 系统默认采用#
作为注释行,Windows 系统默认采用;
作为注释行。
2. 程序实现
2.1 初始化函数 init
在 linux 系统中,ini
配置文件的注释符为 #
,而在 windows 系统中,ini
配置文件的注释符为 ;
,init()
函数的作用就在于在监听配置文件前,先根据执行程序的系统来标识配置文件相对应的注释符。
var hashmark string // 注释符
func init() {
sysType := runtime.GOOS
if sysType == "linux" {
// LINUX系统
hashmark = "#"
}
if sysType == "windows" {
// windows系统
hashmark = ";"
}
}
2.2 结构体 Config
// Config 结构体,定义了文件路径和map存储数据
type Config struct {
Filepath string // 配置文件路径
Conflist []map[string]map[string]string // 存储配置信息的哈希表数组
}
// 根据文件路径对一个 Config 数据进行初始化
func InitConfig(filepath string) *Config {
c := new(Config)
c.Filepath = filepath
return c
}
2.3 配置读取函数 ReadList
函数 ReadList
实现了将 ini
配置文件中的信息转化为哈希表数组的功能,其中数组 Conflist
的每一项为一个哈希表,存储配置文件中一条 section-key-value 信息。在该函数中,按行读取配置文件,并根据行的不同类型执行不同的处理:
- 空行、注释行:不进行处理;
- section 行:记录 section 名,该行以下的 key-value 行则为该 section 的配置信息;
- key-value 行:将 section-key-value 的信息添加到数组中;
func (c *Config) ReadList() ([]map[string]map[string]string, error) {
file, err := os.Open(c.Filepath)
if err != nil {
return nil, fmt.Errorf("Error in ReadList func, can't open the file ") // self define error
}
defer file.Close()
var data map[string]map[string]string
var sectionName string
buf := bufio.NewReader(file)
for {
l, err := buf.ReadString('\n')
line := strings.TrimSpace(l)
// 错误处理
if err != nil {
if err != io.EOF {
return nil, fmt.Errorf("Error in ReadList func, can't Read the file data ") // self define error
}
if len(line) == 0 {
break
}
}
// 对不同类型的行执行不同处理
switch {
case len(line) == 0: //empty line
case string(line[0]) == hashmark: // note line
case line[0] == '[' && line[len(line)-1] == ']':
sectionName = strings.TrimSpace(line[1 : len(line)-1])
data = make(map[string]map[string]string)
data[sectionName] = make(map[string]string)
default:
i := strings.IndexAny(line, "=")
if i == -1 {
fmt.Println("i = -1")
continue
}
if sectionName == "" {
sectionName = "defaultsection"
}
// 将每一条 section-key-value 配置信息添加到数组中
data = make(map[string]map[string]string)
data[sectionName] = make(map[string]string)
keyName := strings.TrimSpace(line[0:i])
keyValue := strings.TrimSpace(line[i+1 : len(line)])
data[sectionName][keyName] = keyValue
if c.unique(sectionName, keyName) == true {
c.Conflist = append(c.Conflist, data)
}
}
}
return c.Conflist, nil
}
2.4 监听函数 ListenFunc
在实现监听函数之前,先定义相关的全局变量、函数类型及接口:
// 被监听section,用于特定 section 的监听
var listenedSection string
// 监听函数类型 ListenFunc
type ListenFunc func(inifile string) (*Config, error)
// Listener接口,监听函数 ListenFunc 需实现该接口
type Listener interface {
Listen(inifile string)
}
// ListenFunc 的 Listener 接口实现
func (f ListenFunc) Listen(inifile string) (*Config, error) {
return f(inifile)
}
接下来实现两个监听函数 ListenFunc
,分别为监听整个配置文件是否修改的全局监听函数 Filelisten
,以及监听配置文件指定 section 是否修改的局部监听函数 Optionlisten
. 在监听函数执行的过程中,会循环读取配置文件,并比较前后两次读取的配置文件(或 section 部分)是否相同,若发生了改变,则将新的配置进行返回.
/*===================== Filelisten =========================*/
func Filelisten(inifile string) (*Config, error) {
conf := InitConfig(inifile)
out, err := conf.ReadList()
if err != nil {
return nil, err
}
var equal bool = false
for {
reconf := InitConfig(inifile)
reout, err := reconf.ReadList()
if err != nil {
return nil, err
}
// 检查配置项的数量是否变化
if len(out) != len(reout) {
c := InitConfig(inifile)
c.Conflist = reout
return c, nil
}
// 若项数相同,则逐项对比
for _, sectionMap := range reout {
for sectionName, keyMap := range sectionMap {
for keyName, keyValue := range keyMap {
equal = false
for _, oldSectionMap := range out {
oldKeyMap := oldSectionMap[sectionName]
oldKeyValue := oldKeyMap[keyName]
if oldKeyValue == keyValue {
equal = true
}
}
// 若有一项不等,则配置文件必定进行了修改
if equal == false {
c := InitConfig(inifile)
c.Conflist = reout
return c, nil
}
}
}
}
time.Sleep(1000)
}
}
/*===================== Optionlisten =========================*/
func Optionlisten(inifile string) (*Config, error) {
conf := InitConfig(inifile)
out, err := conf.ReadList()
if err != nil {
return nil, err
}
var equal bool = false
for {
reconf := InitConfig(inifile)
reout, err := reconf.ReadList()
if err != nil {
return nil, err
}
// 检查配置项的数量是否变化
renum := 0
oldnum := 0
for _, val := range reout {
for valnumname, _ := range val {
if valnumname == mapstr1 {
renum++
}
}
}
for _, val := range out {
for valnumname2, _ := range val {
if valnumname2 == mapstr1 {
oldnum++
}
}
}
if oldnum != renum {
c := SetConfig(infile)
c.conflist = out
return c, nil
}
// 若项数相同,则逐项对比
for _, sectionMap := range reout {
for sectionName, keyMap := range sectionMap {
if sectionName == listenedSection {
for keyName, keyValue := range keyMap {
equal = false
for _, oldSectionMap := range out {
for oldSectionName, _ := range oldSectionMap {
if oldSectionName == listenedSection {
oldKeyMap := oldSectionMap[sectionName]
oldKeyValue := oldKeyMap[keyName]
if oldKeyValue == keyValue {
equal = true
}
}
}
}
// 若有一项不等,则配置文件必定进行了修改
if equal == false {
c := InitConfig(inifile)
c.Conflist = reout
return c, nil
}
}
}
}
}
time.Sleep(1000)
}
}
2.5 监听函数 Watch、WatchWithOption
Watch
与 WatchWithOption
两个函数分别对监听函数 Filelisten
和 Optionlisten
进行了简单封装,简化了函数调用,而监听功能并不发生改变.
// Watch 监听配置文件整个文件是否改变
func Watch(filename string, Listener ListenFunc) (*Config, error) {
Listener = Filelisten
return Listener.Listen(filename)
}
// WatchWithOption 监听配置文件某个 section 是否改变
func WatchWithOption(filename string, Listener ListenFunc, section string) (*Config, error) {
listenedSection = section
Listener = Optionlisten
return Listener.Listen(filename)
}
2.6 辅助函数 unique
unique
函数用于判断配置项“section-key”是否尚未存在,若未存在则返回 true
,若已存在则返回 false
. 用于添加新配置项前的检查. unique
函数会遍历配置数组的每一个配置项,查看配置项是否已经存在,并返回相应的布尔值.
func (c *Config) unique(section string, key string) bool {
for _, sectionMap := range c.Conflist {
for sectionName, keyMap := range sectionMap {
if sectionName == section {
for keyName, _ := range keyMap {
if keyName == key {
return false
}
}
}
}
}
return true
}
3. 单元测试
3.1 辅助数据及函数
var res []map[string]map[string]string // 存储watch返回的配置数组Conflist
var sectionres []map[string]map[string]string // 存储WatchWithOption返回的配置数组Conflist
// 写配置文件,执行Watch,并存储配置数组Conflist
func watchres(lis ListenFunc) {
writeString := "e = f\n#noteline\n[sec]\nsecbasic = basic"
var d1 = []byte(writeString)
ioutil.WriteFile("./config.ini", d1, 0666) //写入文件(字节数组)
a, _ := Watch("config.ini", lis)
res = a.Conflist
}
// 写配置文件,执行WatchWithOption,并存储配置数组Conflist
func watchoptionres(lis ListenFunc) {
writeString := "c = d\n#noteline\n[sec]\nsecbasic = basic"
var d1 = []byte(writeString)
ioutil.WriteFile("./config.ini", d1, 0666) //写入文件(字节数组)
a, _ := WatchWithOption("config.ini", lis, "sec")
sectionres = a.Conflist
}
// 修改配置文件
func changefile() {
writeString := "a = c\n#noteline\n[sec]\nsecindex = index\n"
var d1 = []byte(writeString)
ioutil.WriteFile("./config.ini", d1, 0666) //写入文件(字节数组)
}
3.2 Watch函数测试
创建 goroutine(协程)执行函数 changefile()
,同时并行执行 watchres()
,在 watchres()
函数中,将以下内容写入配置文件:
e = f
#noteline
[sec]
secbasic = basic
TestWatch
函数实现如下:
func TestWatch(t *testing.T) {
var lis ListenFunc = Filelisten
go changefile()
watchres(lis)
if res[0]["defaultsection"]["a"] != "c" || res[1]["sec"]["secindex"] != "index" {
t.Errorf("Test watch error%s, %s", res[0]["defaultsection"]["a"], res[1]["sec"]["secindex"])
}
}
执行该测试,按照预期,res
存储的内容为:
res[0]["defaultsection"]["a"] == "c"
res[1]["sec"]["secindex"] == "index"
若不满足以上要求,说明函数 Watch
实现有错误,输出错误信息.
测试结果:
3.3 WatchWithOption函数测试
与 Watch
函数的测试类似,WatchWithOption
测试时也需要创建协程执行函数 changefile()
,同时并行执行函数 watchoptionres
,在 watchoptionres
函数中,将以下内容写入配置文件:
c = d
#noteline
[sec]
secbasic = basic
TestWatchWithOption
函数实现如下:
func TestWatchWithOption(t *testing.T) {
var lis ListenFunc = Optionlisten
go changefile()
watchoptionres(lis)
if sectionres[0]["defaultsection"]["a"] != "c" || sectionres[1]["sec"]["secindex"] != "index" {
t.Errorf("Test watch error%s, %s", sectionres[0]["defaultsection"]["a"], sectionres[1]["sec"]["secindex"])
}
}
执行该测试,按照预期,sectionres
存储的内容为:
sectionres[0]["defaultsection"]["a"] == "c"
sectionres[1]["sec"]["secindex"] == "index"
若不满足以上要求,说明函数 WatchWithOption
实现有错误,输出错误信息.
3.4 其他函数测试
-
InitConfig
函数测试
-
ReadList
函数测试
-
Unique
函数测试
4. 使用案例
4.1 使用 Watch 函数
运行以下 main
函数:
func main() {
var listen ListenFunc = Filelisten //or Optionlisten
inifile := "config2.ini"
a, err := Watch(inifile, listen)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println(a)
}
}
运行时注释 data
的 “key-value” 对,运行结果如下:
4.2 使用 WatchWithOption 函数
运行以下 main
函数:
func main() {
var listen ListenFunc = Optionlisten //or Optionlisten
inifile := "config2.ini"
a, err := WatchWithOption(inifile, listen, "server")
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println(a)
}
}
运行时注释 http_port
的 “key-value” 对,运行结果如下:
5. godoc自动生成API文档
在终端执行 $ godoc
命令,即可在 http://localhost:6060 打开自动生成的本地 $GOPATH/src
目录下项目的API文档,如图: