设计要求
-
核心任务:包必须提供一个函数 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 系统默认采用 ; 作为注释行。
-
不能使用第三方包,但可以参考、甚至复制它们的代码。例如:
设计说明
根据给出的实现要求,我们的读取配置文件包主要就是这两个部分:
- 第一,实现接口调用Listen监听文件内容是否改变(在Watch运行的时间内),控制输出
- 第二实现Watch函数,它要能读取配置文件,并把它放到合适的结构体(自定义,参考官方文档),并返回
所以,就有了以下设计思路:
定义接口和结构体
/*Listener is a interface
*监听配置文件是否被修改
*/
type Listener interface {
//Listen is the method to complete
Listen(inifile string)
}
/*Element used for saving key-value pair
*使用map来存储对应的键值对
*/
type Element map[string]string
/*Configuration used for saving section
*要返回的配置文件结构
*/
type Configuration map[string][]Element
Init函数
区分Linux与Windos的注释符
//定义注释
var comment byte
/*Init is a func
*使得 Unix 系统默认采用 # 作为注释行
*Windows 系统默认采用 ; 作为注释行
*/
func Init() {
system := runtime.GOOS
if system == "linux" {
comment = '#'
}
if system == "windows" {
comment = ';'
}
}
Watch函数的实现
具体逻辑就是首先读文件,然后逐行判断是否是注释,还是section,还是key-value对。在对应的判断中执行相应的操作。
至于监听这个功能,并不是我们要放在包中的内容,所以我建了一个main.go,用于实现具体的接口方法,并且反应监听功能。
/*Watch is a func
*监听配置文件是否被修改,返回最新的配置文件内容
*只能监听到一次变化
*/
func Watch(filename string, listener Listener) (Configuration, error) {
listener.Listen(filename)
result := make(Configuration)
//myErr 为自定义错误类型
var myErr error = nil
file, err := os.Open(filename)
//读取文件失败
if err != nil {
myErr = errors.New("failed to open file.")
return result, myErr
}
defer file.Close()
//将读取的ini文件转换成一个bufio
//然后解析到对应的section
reader := bufio.NewReader(file)
//section 初始化为空
section := ""
for {
//以'\n'作为结束符读入行
//并判断文件是否读完
//判断是否成功读取
line, err := reader.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
myErr = errors.New("failed to read file in line.")
break
}
//解析行中的内容
//空行就继续
//一定要先把两端的空白去掉,不然在匹配字符串的时候会有奇怪问题
line = strings.TrimSpace(line)
if line == "" {
continue
}
//提取注释
if line[0] == comment {
continue
}
length := len(line)
//将读入的每一行进行匹配
if line[0] == '[' && line[length-1] == ']' {
//匹配section
section = line[1 : length-1]
//判断map中有没有这个section
if _, in := result[section]; !in {
result[section] = []Element{}
}
} else {
//以‘=’分割字符串
//得到key,value
str := strings.Split(line, "=")
if len(str) < 2 {
myErr = errors.New("unvalid key-value pair.")
break
}
key := str[0]
value := str[1]
element := make(Element)
element[key] = value
//把键值对添加进section
if section == "" {
result[section] = []Element{}
}
if _, correct := result[section]; correct {
result[section] = append(result[section], element)
}
}
}
return result, myErr
}
main.go的实现
轮循判断文件是否改变,并给一个最大执行时间为20s.
package main
// 主函数
import (
"fmt"
"os"
"time"
"github.com/github-user/iniRead/read"
)
type ListenFunc func(string)
func (l ListenFunc) Listen(inifile string) {
l(inifile)
}
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("The file has been changed ", inifile)
break
}
time.Sleep(time.Duration(1) * time.Second)
}
}
func main() {
go func() {
read.Init()
for {
conf, err := read.Watch("../test.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(20) * time.Second)
}
单元测试
read_test.go
代码如下:
package read
import (
"fmt"
"os"
"testing"
"time"
)
type ListenFunc func(string)
func (l ListenFunc) Listen(inifile string) {
l(string(inifile))
}
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("The file has been changed ", inifile)
break
}
time.Sleep(time.Duration(1) * time.Second)
}
}
func TestInit(t *testing.T) {
var expected byte
expected = '#'
Init()
get := getComment()
if expected != get {
t.Errorf("expected '%q' but got '%q'", expected, get)
}
}
var l1 Listener = lis
func TestWatch(t *testing.T) {
tests := []struct {
name string
file string
listener Listener
}{
// TODO: Add test cases
{
name: "case 1",
file: "../test1.ini",
listener: l1,
},
{
name: "case 2",
file: "../test2.ini",
listener: l1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Watch(tt.file, tt.listener)
})
}
}
测试结果
注意,在测试的时候也是需要去主动修改test1.ini和test2.ini的内容才有输出
main_test.go
代码如下:
package main
import "testing"
func TestListenFunc(t *testing.T) {
tests := []struct {
name string
file string
}{
// TODO: Add test cases
{
name: "case 1",
file: "../test1.ini",
},
{
name: "case 2",
file: "../test2.ini",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lis(tt.file)
})
}
}
测试结果
功能测试
进入mian路径下,直接执行go run main.go
需要注意的一点是,我的Watch函数只有在文件有改动的时候才有返回值,所以你需要在20s内(这时我设置的进程终止时间,防止轮循一直不结束)修改配置文件的内容才会有输出
使用Godoc生成API文档
由于golang.org不能正常访问,所以推荐使用代理安装godoc
GOPROXY=https://mirrors.aliyun.com/goproxy/ GO111MODULE=on go get golang.org/x/tools/cmd/godoc
然后就可以直接到read.go的目录下使用godoc命令生成API文档了
浏览器访问http://localhost:6060/pkg/github.com/github-user/iniRead/read/就看到当前的API文档为: