基于Golang的监听&读取配置文件的程序包开发——simpleConfig_v1 【阅读时间:约10分钟】
一、配置文件概述
配置文件(Configuration File,CF)是一种文本文档,为计算机系统或程序配置参数和初始设置。传统的配置文件就是文本行,在 Unix 系统中随处可见,通常使用 .conf
,.config
,.cfg
作为后缀,并逐步形成了 key = value
的配置习惯。在 Windows 系统中添加了对 section
支持,通常用 .ini
作为后缀。面向对象语言的兴起,程序员需要直接将文本反序列化成内存对象作为配置,逐步提出了一些新的配置文件格式,包括 JSON,YAML,TOML 等。
本次监听&读取配置文件的程序包()开发,主要应用于ini配置文件。
开发过程中使用的配置文件config.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.系统环境
操作系统:CentOS7
硬件信息:使用virtual box配置虚拟机(内存3G、磁盘30G)
编程语言:GO 1.15.2
2.项目的任务要求
-
核心任务:包必须提供一个函数
Watch(filename,listener) (configuration, error)
- 输入 filename 是配置文件名
- 输入 listener 一个特殊的接口,用来监听配置文件是否被修改,让开发者自己决定如何处理配置变化
type ListenFunc func(string)
type inteface Listener { listen(inifile string) }
ListenFunc
实现接口方法listen
直接调用函数- 优点
- 所有满足签名的函数、方法都可以作为参数
- 所有实现 Listener 接口的数据类型都可作为参数
- 输出 configuration 数据类型,可根据 key 读对应的 value。 key 和 value 都是字符串
- 输出 error 是错误数据,如配置文件不存在,无法打开等
- 可选的函数
WatchWithOption(filename,listener,...) (configuration, error)
-
包必须包括以下内容:
- 生成的中文 api 文档
- 有较好的 Readme 文件,包括一个简单的使用案例
- 每个go文件必须有对应的测试文件
- 必须提供自定义错误
- 使有 init 函数,使得 Unix 系统默认采用
#
作为注释行,Windows 系统默认采用;
作为注释行。
-
不能使用第三方包,但可以参考、甚至复制它们的代码。例如:
三、具体程序设计及Golang代码实现
根据任务要求可知,simpleConfig_v1
程序包中的watch函数相当于load、read、listen这三个函数的结合。在调用该函数时,首先会输出配置文件的原始信息,然后会一直监听配置文件有无改动,若有改动则会提示并展示最新的配置文件信息。
simpleConfig_v1
程序包的函数架构如下:
下面按照simpleConfig_v1
程序包的源码顺序来依次介绍数据结构和相关函数。
1. 数据结构
var sys string
var flag int
//three layer, like [server] -> protocol -> http
type Config [](map[string](map[string]string))
sys用于标记注释行, Unix 系统默认采用 #
作为注释行,Windows 系统默认采用 ;
作为注释行。
flag用于标记watch函数只能输出一次原始配置文件的信息。
Config是配置文件的数据结构,一个简单的配置文件最多可有三层,比如:
FirstLayer->SecondLayer->ThirdLayer
[server] -> protocol -> http
2. init函数模块
func init() {
flag = 0
if runtime.GOOS == "windows" {
sys = ";"
} else {
sys = "#"
}
}
使有 init 函数,使得 Unix 系统默认采用 #
作为注释行,Windows 系统默认采用 ;
作为注释行。
3.listen函数模块
type Listener interface {
Listen(filename string)
}
type ListenFunc func(filename string) (Config, error)
func (fun ListenFunc) Listen(filename string) (Config, error) {
return fun(filename)
}
func Watch(filename string, listener ListenFunc) (Config, error) {
...
//listen
return listener.Listen(filename)
}
func main() {
var listener ListenFunc = OnConfigChange
filename := "config.ini"
for {
configuration, err := Watch(filename, listener)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println(filename + "文件发生改变,改变后的配置信息如下:")
//fmt.Println(configuration)
for key, value := range configuration {
fmt.Println(key, ":", value)
}
fmt.Println("")
}
time.Sleep(time.Duration(2) * time.Second)
}
}
输入 listener 一个特殊的接口,用来监听配置文件是否被修改,让开发者自己决定如何处理配置变化。
- type ListenFunc func(string)
- type inteface Listener { listen(inifile string) }
- ListenFunc
实现接口方法 listen
直接调用函数
- 优点
- 所有满足签名的函数、方法都可以作为参数
- 所有实现 Listener 接口的数据类型都可作为参数
在上述例子中,listen函数的执行流程为
watch函数 -> listener.Listen(filename)函数 -> OnConfigChange函数
其中OnConfigChange函数具体实现如下:
func OnConfigChange(filename string) (Config, error) {
temp := new(Config)
config1, err := temp.ReadConfig(filename)
if err != nil {
return config1, err
}
flag2 := false
for {
temp2 := new(Config)
config2, err := temp2.ReadConfig(filename)
if err != nil {
return config2, err
}
if len(config1) != len(config2) {
return config2, nil
}
for _, i := range config2 {
for j, k := range i {
for l, m := range k {
flag2 = false
for _, n := range config1 {
map1 := n[j]
map2 := map1[l]
if map2 == m {
flag2 = true
}
}
if flag2 == false {
return config2, nil
}
}
}
}
}
}
每当listen函数模块监听到配置文件由发生修改,便会提示修改信息和输出修改后的配置文件信息。
4.watch函数模块
//Watch = load + read + listen
func Watch(filename string, listener ListenFunc) (Config, error) {
//load + read
if flag == 0 {
config := new(Config)
configuration, err := config.ReadConfig(filename)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("")
fmt.Println(filename + "文件原始的的配置信息如下:")
//fmt.Println(configuration)
for key, value := range configuration {
fmt.Println(key, ":", value)
}
fmt.Println("")
}
flag = 1
}
//listen
return listener.Listen(filename)
}
watch函数相当于load、read、listen这三个函数的结合。在调用该函数时,首先会输出配置文件的原始信息,然后会一直监听配置文件有无改动,若有改动则会提示并展示最新的配置文件信息。
其中load&read的函数为ReadConfig函数,其具体实现如下:
func (c *Config) ReadConfig(filename string) (Config, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var element map[string]map[string]string
var FirstLayer string
buf := bufio.NewReader(file)
for {
l, err := buf.ReadString('\n')
line := strings.TrimSpace(l)
if err != nil {
if err != io.EOF {
return nil, err
}
if len(line) == 0 {
break
}
}
switch {
case len(line) == 0:
case string(line[0]) == sys:
case line[0] == '[' && line[len(line)-1] == ']':
FirstLayer = strings.TrimSpace(line[1 : len(line)-1])
element = make(map[string]map[string]string)
element[FirstLayer] = make(map[string]string)
default:
index := strings.IndexAny(line, "=")
value := strings.TrimSpace(line[index+1 : len(line)])
if FirstLayer == "" {
FirstLayer = "FirstLayer"
}
element = make(map[string]map[string]string)
element[FirstLayer] = make(map[string]string)
valmap := strings.TrimSpace(line[0:index])
element[FirstLayer][valmap] = value
*c = append(*c, element)
}
}
return *c, nil
}
四、设置自定义错误
利用errors包,可以在【三】的基础上添加自定义错误如下:
func OnConfigChange(filename string) (Config, error) {
temp := new(Config)
config1, err := temp.ReadConfig(filename)
if err != nil {
err2 := errors.New("Could not read the config file.")
return config1, err2
}
...
temp2 := new(Config)
config2, err := temp2.ReadConfig(filename)
if err != nil {
err2 := errors.New("Could not read the config file.")
return config2, err2
}
...
}
func (c *Config) ReadConfig(filename string) (Config, error) {
file, err := os.Open(filename)
if err != nil {
err2 := errors.New("Could not open the config file.")
return nil, err2
}
...
l, err := buf.ReadString('\n')
line := strings.TrimSpace(l)
if err != nil {
if err != io.EOF {
err2 := errors.New("Could not read the config element.")
return nil, err2
}
if len(line) == 0 {
break
}
}
...
}
五、程序测试
1.封装并使用程序包
将项目simpleConfig_v1的simpleConfig_v1.go文件的main函数注释掉,package改为package simpleConfig_v1,然后执行如下指令:
go build
在其他路径下建立main.go,内容如下(listen函数可由用户自定义设置,此处使用simpleConfig_v1自带的OnConfigChange函数):
//main.go
package main
import (
"fmt"
"time"
"github.com/user/simpleConfig_v1"
)
func main() {
var listener simpleConfig_v1.ListenFunc = simpleConfig_v1.OnConfigChange
filename := "config.ini"
for {
configuration, err := simpleConfig_v1.Watch(filename, listener)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println(filename + "文件发生改变,改变后的配置信息如下:")
//fmt.Println(configuration)
for key, value := range configuration {
fmt.Println(key, ":", value)
}
fmt.Println("")
}
time.Sleep(time.Duration(2) * time.Second)
}
}
并在main.go的目录下存放config.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
2.功能测试
功能测试主要从用户角度测试程序包的功能,步骤如下:
[henryhzy@localhost user]$ go run main.go
config.ini文件原始的的配置信息如下:
0 : map[FirstLayer:map[app_mode:development]]
1 : map[paths:map[data:/home/git/grafana]]
2 : map[server:map[protocol:http]]
3 : map[server:map[http_port:9999]]
4 : map[server:map[enforce_domain:true]]
config.ini文件发生改变,改变后的配置信息如下:
0 : map[FirstLayer:map[app_mode:development]]
1 : map[paths:map[data:/home/git/grafana]]
2 : map[server:map[protocol:http]]
3 : map[server:map[http_port:18342026]]
4 : map[server:map[enforce_domain:true]]
config.ini文件发生改变,改变后的配置信息如下:
0 : map[FirstLayer:map[app_mode:henryhzy]]
1 : map[paths:map[data:/home/git/grafana]]
2 : map[server:map[protocol:http]]
3 : map[server:map[http_port:18342026]]
4 : map[server:map[enforce_domain:true]]
^Csignal: interrupt
由此可知程序包的功能测试正常,调用程序包后首先会输出配置文件的原始信息,然后会一直监听配置文件有无改动,若有改动则会提示并展示最新的配置文件信息。通过键盘ctrl+c可以终止监听程序。
3.单元测试
单元测试主要从程序员角度,对程序包的具体函数进行测试。
①init函数
测试代码:
func Test_init(t *testing.T) {
var test_sys string = "#"
got := sys
want := test_sys
if got != want {
t.Errorf("\n got %s\n want %s\n", got, want)
}
}
测试结果:
②ReadConfig函数
测试代码:
func Test_ReadConfig(t *testing.T) {
filename := "config.ini"
temp := new(Config)
_, err := temp.ReadConfig(filename)
if err != nil {
t.Errorf("ReadConfig function failed\n%s\n", err)
}
}
测试结果:
③listen函数
测试代码:
func Test_listen(t *testing.T) {
var listener ListenFunc = OnConfigChange
filename := "error_name.ini"
_, err := listener(filename)
got := fmt.Sprintf("%s", err)
err2 := errors.New("Could not read the config file.")
want := fmt.Sprintf("%s", err2)
if got != want {
t.Errorf("\nListen function failed\n%s\n%s", err, err2)
}
}
测试结果:
④watch函数
测试代码:
func Test_watch(t *testing.T) {
var listener ListenFunc = OnConfigChange
filename := "error_name.ini"
_, err := Watch(filename, listener)
got := fmt.Sprintf("%s", err)
err2 := errors.New("Could not read the config file.")
want := fmt.Sprintf("%s", err2)
if got != want {
t.Errorf("\nListen function failed\n%s\n%s", err, err2)
}
}
测试结果:
通过简单的单元测试可知,程序包的函数均可正常调用。
六、中文 api 文档
首先安装godoc如下:
git clone https://github.com/golang/tools $GOPATH/src/golang.org/x/tools
go build golang.org/x/tools
将项目simpleConfig_v1的simpleConfig_v1.go文件的main函数注释掉,package改为package simpleConfig_v1,然后执行如下指令:
go install
go doc
godoc -url="pkg/github.com/user/simpleConfig_v1" > API.html
便会在当前目录下生成API.html文件:
七、完整代码
具体代码可见gitee仓库:gitee