1、概述
配置文件(Configuration File,CF)是一种文本文档,为计算机系统或程序配置参数和初始设置。传统的配置文件就是文本行,在 Unix 系统中随处可见,通常使用.conf
,.config
,.cfg
作为后缀,并逐步形成了key = value
的配置习惯。在 Windows 系统中添加了对section
支持,通常用.ini
作为后缀。面向对象语言的兴起,程序员需要直接将文本反序列化成内存对象作为配置,逐步提出了一些新的配置文件格式,包括 JSON,YAML,TOML 等。
2、课程任务
任务目标
- 熟悉程序包的编写习惯(idioms)和风格(convetions)
- 熟悉 io 库操作
- 使用测试驱动的方法
- 简单 Go 程使用
- 事件通知
任务内容
在 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
任务要求
ini读写包、Viper 读配置集成解决方案包、fsnotify 文件系统通知包
完成任务
新建一个包readini,里面实现函数Watch(filename,listener) (configuration, error)
。
首先要设计相应的数据结构,根据任务要求,我们可设计成这样。
// Section下面的键值对
type Element map[string]string
// ini文件结构(对象)
// Object为各个Section所对应的所有键值对
type Configuration map[string][]Element
// Listener接口
type Listener interface {
Listen(inifile string)
}
同时,Unix系统默认采用#
作为注释行,Windows系统默认采用;
作为注释行。因此还需要有init函数对当前系统的注释符作初始化。
// 当前系统下的注释符
var CommentSymbol byte
// 通过确定当前操作系统,从而确定相应的注释符
func Init() {
sysType := runtime.GOOS
if sysType == "linux" {
CommentSymbol = '#'
}
if sysType == "windows" {
CommentSymbol = ';'
}
}
随后便可以设计Watch函数了。
// 监听自函数运行以来发生的一次配置文件变化并返回最新的配置文件解析内容。
func Watch(filename string, listener Listener) (Configuration, error) {
listener.Listen(filename)
i := make(Configuration)
var e error = nil
f, err := os.Open(filename)
if err != nil {
e = errors.New("Open file faild.")
return i, e
}
defer f.Close()
// 将ini文件转换成一个bufio
r := bufio.NewReader(f)
// 当前所解析到的section
section := ""
for {
// 以'\n'作为结束符读入一行
line, err := r.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
e = errors.New("Read file faild.")
break
}
// 删除行两端的空白字符
line = strings.TrimSpace(line)
// 解析一行中的内容
// 空行则跳过
if line == "" {
continue
}
// 以符号CommentSymbol作为注释
if line[0] == CommentSymbol {
continue
}
length := len(line)
// 匹配字符串
if line[0] == '[' && line[length-1] == ']' { // section
section = line[1 : length-1]
// 如果map中没有这个section,添加进来
if _, ok := i[section]; !ok {
i[section] = []Element{}
}
}else { // 键值对数据
// 分割字符串
s := strings.Split(line, "=")
if len(s) < 2 {
e = errors.New("Incorrect key-value pair format.")
break
}
key := strings.TrimSpace(s[0])
value := strings.TrimSpace(s[1])
element := make(Element)
element[key] = value
// 把键值对添加进section里面
if section == "" {
i[section] = []Element{}
}
if _, ok := i[section]; ok {
i[section] = append(i[section], element)
}
}
}
return i, e
}
先是通过listener来监听配置文件的变化,(我后续的测试用的是自旋锁的方式)。一旦检测到变化,就立即跳出自旋锁,解析其中的section键值对,最终返回解析内容Configuration和错误error。同时自定义错误error用的是errors
库下的New(string)
函数来确定相应的错误信息。
自此,一个简单的readini包就完成了,里头提供一个函数Watch(filename,listener) (configuration, error)
。
最后执行指令go build github.com/github-user/test/readini
,其他程序就能够通过import "github.com/github-user/test/readini"
的方式来导入这个包了。
测试
readini包里也就只有两个函数,Init
跟Watch
,分别针对两个函数写一个测试函数。
func ExampleInit() {
Init()
fmt.Printf("%c\n",CommentSymbol)
//Output:#
}
type ListenFunc func(string)
func (l ListenFunc) Listen(inifile string) {
l(string(inifile))
}
func ExampleWatch() {
var lis ListenFunc = func(inifile string) {
before_info, err := os.Lstat(inifile)
if err != nil {
panic(err)
}
for {
after_info, err := os.Lstat(inifile)
if err != nil {
panic(err)
}
if !before_info.ModTime().Equal(after_info.ModTime()) {
break
}
time.Sleep(time.Duration(1)*time.Second)
}
}
go func(){
conf, err := Watch("example.ini", lis)
if err != nil {
fmt.Println(err)
}
for s, _ := range conf {
fmt.Println("Section: ", s)
for _, value := range conf[s] {
for k, v := range value {
fmt.Println("Key:", k, "\tValue:", v)
}
}
fmt.Println()
}
}()
file,openErr:=os.Create("example.ini")
defer os.Remove("example.ini")
defer file.Close()
if openErr!=nil {
panic(openErr)
}
time.Sleep(time.Duration(1)*time.Second)
writer:=bufio.NewWriter(file)
_,errWrite := writer.Write([]byte("[test]\n"))
_,errWrite = writer.Write([]byte("value1 = 123\n"))
_,errWrite = writer.Write([]byte("value2 = 222\n"))
if errWrite!=nil {
os.Exit(0)
}
writer.Flush()
time.Sleep(time.Duration(2)*time.Second)
/*Output:
Section: test
Key: value1 Value: 123
Key: value2 Value: 222
*/
}
在ExampleWatch里面,主要思想就是通过一个go程来执行Watch函数,将返回的Configuration打印出来。使用go程的原因是因为listen是一个自旋锁的实现方式,需要在外面对文件稍作更新来触发的话就不能把主线程跟阻塞了。
这里最需要注意的是延时函数的使用,确保语句执行的先后次序,避免后面的文件写操作执行的过快,使得go程里检测不到更新。我在这里也是吃了点苦头
单元测试完之后,本应该是轮到集成测试。但是由于我这里只有两个函数,且Init函数相对于Watch函数来说实在是有点简单。即便是集成之后的测试,跟单独Watch函数的测试也差不了多少,因而在这里我就没在写集成之后的了。在后续的功能测试里面其实也可以看到集成之后的效果。
因为这里要设计的是一个readini包,既然是包的话就应该可以在其他包里面通过import的方式来导入。
于是,额外创建一个main.go
来测试对这个包函数的调用。
package main
// 主函数
import (
"fmt"
"os"
"time"
"bufio"
"github.com/github-user/test/readini"
)
type ListenFunc func(string)
func (l ListenFunc) Listen(inifile string) {
l(inifile)
}
func main() {
var lis ListenFunc = func(inifile string) {
before_info, err := os.Lstat(inifile)
if err != nil {
panic(err)
}
for {
after_info, err := os.Lstat(inifile)
if err != nil {
panic(err)
}
if !before_info.ModTime().Equal(after_info.ModTime()) {
fmt.Println("There are something changed in file ", inifile)
break
}
fmt.Println("Listening changes in file ", inifile)
time.Sleep(time.Duration(1)*time.Second)
}
}
go func(){
readini.Init()
for {
conf, err := readini.Watch("../111.ini", lis)
if err != nil {
fmt.Println(err)
}
for s, _ := range conf {
fmt.Println("Section: ", s)
for _, value := range conf[s] {
for k, v := range value {
fmt.Println("Key:", k, "\tValue:", v)
}
}
fmt.Println()
}
}
}()
time.Sleep(time.Duration(3)*time.Second)
i := 0
file,openErr := os.OpenFile("../111.ini", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if openErr != nil {
panic(openErr)
}
writer := bufio.NewWriter(file)
_, errWrite := writer.Write([]byte("[test]\n"))
if errWrite != nil {
panic(errWrite)
}
writer.Flush()
for {
_, errWrite = writer.Write([]byte(fmt.Sprintf("Test_key%d = Test_value%d\n", i, i)))
i += 1
if errWrite != nil {
panic(errWrite)
}
writer.Flush()
time.Sleep(time.Duration(3)*time.Second)
}
}
在前面执行完go build github.com/github-user/test/readini
指令之后,这个程序运行起来也还是没有问题的。
跟Watch函数的测试有点类似,这里创建一个go程,在Init初始化之后,持续执行Watch函数,一旦监听到配置文件的变化就输出最新的解析内容。
同时在主线程里面,每个3秒就新写入一个键值对,因而,在执行指令go run main.go
之后,我们能够看到的是大约每隔3秒就检测到配置文件发生一次变化,并输出最新的解析内容。
自此,功能实现的也就差不多了。
生成API文档
先通过指令go get golang.org/x/tools/cmd/godoc
来安装godoc。因为这条指令是通过到官网下载的,因而很大可能会访问超时,这时候可以先连接上中国的Go模块代理,也就是依次执行指令export GO111MODULE=on
和export GOPROXY=https://goproxy.cn
,然后再执行go get就可以了(最好别写入~/.profile
文件里面,不然后续的Go模块很可能就会突然发生一系列不可描述的错误,我已踩坑)。
然后执行指令go build golang.org/x/tools/cmd/godoc
,再执行godoc
就可以在浏览器通过http://localhost:6060/来访问了,找到相对应的路径即可。(我的话就是http://localhost:6060/pkg/github.com/github-user/test/readini/)
最后,将文档保存出来的话就是指令godoc -url "http://localhost:6060/pkg/github.com/github-user/test/readini/" > api.html
。