服务计算Homework04
- 项目地址
- 使用说明:
Watch
文件夹存放路径为xx/github.com/Watch
,直接运行main.go
即可,注意将配置文件my.ini
和main.go
放在同一目录下
任务目标
- 熟悉程序包的编写习惯(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
任务要求
- 核心任务:包必须提供一个函数
Watch(filename,listener) (configuration, error)
- 输入 filename 是配置文件名
- 输入 listener 一个特殊的接口,用来监听配置文件是否被修改,让开发者自己决定如何处理配置变化
- 输出 configuration 数据类型,可根据 key 读对应的 value。 key 和 value 都是字符串
- 输出 error 是错误数据,如配置文件不存在,无法打开等
- 可选的函数
WatchWithOption(filename,listener,...) (configuration, error)
- 包必须包括以下内容:
- 生成的中文 api 文档
- 有较好的 Readme 文件,包括一个简单的使用案例
- 每个go文件必须有对应的测试文件
- 必须提供自定义错误
- 使有 init 函数,使得 Unix 系统默认采用
#
作为注释行,Windows 系统默认采用;
作为注释行。
实现过程
1. 实现一个基础的读取ini文件
- 打开文件,如果出错则直接返回
- 读取文件内容并逐行查找键值对
- 注意最后一行可能没有换行符,要做特殊处理
- 返回存储键值对的configuration和自定义错误error
// 读取文件并返回ini的键值对和自定义错误类型
func readFile(filename string) (map[string]string, errorString) {
// 打开文件
var error errorString
file, err := os.Open(filename)
configuration := make(map[string]string)
// 打开文件错误
if err != nil {
error = createErr("error1: failed to open the configuration file")
return configuration, error
} else {
error = createErr("")
}
// 函数结束时关闭文件
defer file.Close()
// 读取文件内容
reader := bufio.NewReader(file)
for {
// 读取一行文件
linestr, err := reader.ReadString('\n')
// 最后一行没有换行符,err==EOF直接退出, 不能读取最后一行
if err != nil && err != io.EOF {
fmt.Println(err)
break
}
// 切掉行的左右两边的空白字符
linestr = strings.TrimSpace(linestr)
// 忽略空行、注释、分段
if linestr == "" || linestr[0] == comment || linestr[0] == '[' {
continue
}
pair := strings.Split(linestr, "=")
// 保证切开只有一个等号的情况
if len(pair) == 2 {
// 去掉多余空白字符
key := strings.TrimSpace(pair[0])
value := strings.TrimSpace(pair[1])
configuration[key] = value
// fmt.Println(key)
}
// 判断是否为最后一行,可改为:err != io.EOF
if err != nil {
// fmt.Println(err)
break
}
}
return configuration, error
}
2. readFile
中用到的自定义错误类型
- 自定义错误数据结构,只有一个成员变量为string类型,记录错误信息
createErr
方法,新创建一个错误,类似于面向对象语言的构造函数Error
方法,输出错误信息,类似于面向对象的成员函数
// 错误信息的数据结构
type errorString struct {
str string
}
// 创建错误信息的方法
func createErr(text string) errorString {
return errorString{text}
}
// 打印错误信息并返回
func (err *errorString) Error() string {
fmt.Fprintf(os.Stdout, err.str)
return err.str
}
3. 接口Listener
的实现
ListenFunc
是函数签名,定义一个参数为string的函数类型Listener
是一个自定义接口,拥有方法listen
- 函数签名
ListenFunc
实现接口Listener
的方法listen
, 使得所有满足签名的函数、方法、所有实现 Listener 接口的数据类型都可作为参数,方便传参实现Watch
函数
// 函数签名
type ListenFunc func(string)
// 自定义接口
type Listener interface {
listen(infile string)
}
//函数签名ListenFunc实现接口方法
func (Listen ListenFunc) listen(inifile string) {
// 调用具体的fileListen
fileListen(inifile)
}
4.具体的fileListen
函数实现
- 将原始文件内容读入切片
originSlice
- 监听文件内容并读入切片
curSlice
- 将
originSlice
和curSlice
的内容逐行比较,如果不等则检测到修改退出监听
// 监听文件
func fileListen(fileName string) {
originSlice := []string{}
originSlice = fileContent(fileName)
flag := false
for {
curSlice := []string{}
curSlice = fileContent(fileName)
for i, value := range originSlice {
if value != curSlice[i] {
flag = true
break
}
}
if flag == true {
break
}
}
}
另一种实现思路是监听文件状态的
修改时间
这一属性,但是可能造成内容没有修改然而修改时间发生改变的情况,所以不采取该实现方法
5. 监听函数Watch
的实现
- 读取文件并打印信息
- 开始监听,如果文件有改变,则退出监听并打印最新文件内容
func Watch(filename string, Listen Listener) (map[string]string, errorString) {
myInit("Windows")
// 第一次读取文件
config, error1 := readFile(filename)
if len(error1.str) != 0 {
fmt.Printf("First - Error occurs : %s", error1)
return config, error1
}
// 打印配置文件信息
printConfig(config)
// 打印原始文件内容
printFile(filename)
fmt.Printf("Listening---------------------------------------------------:\n")
// 开启监听文件
Listen.listen(filename)
// 文件被修改
fmt.Printf("The ini file has been changed :\n")
// 打印修改后文件内容
printFile(filename)
return config, error1
}
5. 其他辅助函数
-
init函数(init与内置函数重名,本文实现用
myInit
函数代替)func myInit(sysType string) { if sysType == "Linux" { comment = '#' } else if sysType == "Windows" { comment = ';' } }
-
打印函数
printConfig
,打印对应的ini文件键值对func printConfig(configure map[string]string) { fmt.Printf("Print configure begin: \n") for key, value := range configure { fmt.Println(key, "=", value) } fmt.Printf("Print configure end\n\n") }
-
打印函数
printFile
,打印指定文件内容func printFile(fileName string) { // 打开文件 file, err := os.Open(fileName) // 打开文件错误 if err != nil { return } // 函数结束时关闭文件 defer file.Close() // 读取文件内容 reader := bufio.NewReader(file) fmt.Println("Print File begin") for { // 读取一行文件 linestr, err := reader.ReadString('\n') fmt.Println(linestr) if err != nil { break } } fmt.Println("Print File end") }
-
打印函数
fileContent
,获取文件内容保存在切片并返回func fileContent(fileName string) []string { // 声明了一个暂不确定长度大小的string切片 // 一个切片在未初始化之前默认为 nil,长度为 0 var strSlice []string file, err := os.Open(fileName) // 打开文件错误 if err != nil { return strSlice } // 函数结束时关闭文件 defer file.Close() // 读取文件内容 reader := bufio.NewReader(file) for { // 读取一行文件 linestr, err := reader.ReadString('\n') strSlice = append(strSlice, linestr) // 错误判断 if err != nil { break } } return strSlice }
6. 结果展示
-
配置文件内容
[core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true hideDotFiles = dotGitOnly [remote "origin"] url = https://github.com/davyxu/cellnet fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master
-
打印ini配置文件键值对
红色方框内不是配置文件内容
-
监听文件内容修改前
-
监听文件内容修改后
-
测试
readFile
,测试文件见项目
-
自动生成的API文档,详见项目
参考资料