服务计算 读简单ini配置文件——watch函数实现

服务计算 读简单ini配置文件

1. 任务介绍

1.1 项目地址

gitee项目传送门

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 任务要求

  1. 核心任务:包必须提供一个函数 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)
  2. 包必须包括以下内容:
    • 生成的中文 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

WatchWatchWithOption 两个函数分别对监听函数 FilelistenOptionlisten 进行了简单封装,简化了函数调用,而监听功能并不发生改变.

// 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文档,如图:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值