服务计算第四次作业
任务目标
- 熟悉程序包的编写习惯(idioms)和风格(convetions)
- 熟悉 io 库操作
- 使用测试驱动的方法
- 简单 Go 程使用
- 事件通知
任务内容
在 Gitee 或 GitHub 上发布一个读配置文件程序包,第一版仅需要读 ini 配置;
任务要求
1. 核心任务: 包必须提供一个函数 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)
2. 包必须包括以下内容:
- 生成的中文 api 文档
- 有较好的 Readme 文件,包括一个简单的使用案例
- 每个go文件必须有对应的测试文件
- 必须提供自定义错误
- 使用 init 函数,使得 Unix 系统默认采用 # 作为注释行,Windows 系统默认采用 ; 作为注释行。
3. 不能使用第三方包,但可以参考、甚至复制它们的代码。例如:
- ini 读写包。 Github,中文支持
- Viper 读配置集成解决方案包。Github
- live watching and re-reading of config files (optional)
- fsnotify 文件系统通知包。 Github
- 你可以参考这些代码,但不能在你的包中 import
任务实现:
自定义Error
由于本次实验要求自定义错误,因此,在readconfigutil包实现的时候,我给出了自己的Error类型:MyconfigError
-
MyconfigError定义
type MyconfigError struct { message string err error }
- message:存储错误信息的字符串;
- err:原始的error,实际在本次实验中并未使用,可以忽略它的存在;
-
MyconfigError实现Error接口
在go中,所有的错误,包括自定义错误都实现了error接口,也必须实现error接口才能将自己的定义的Error类型的变量传值给error类型的参数。否则,如果不使用go的error接口来接收错误,那么代码的兼容性极差,而且编程过程中会多次遇到类型转换的问题,十分麻烦,因此,自己定义的错误类型必须实现error接口;
error接口定义:
type error interface { func Error() string }
为了实现error接口,我们必须为自定义的错误类型:
MyconfigError
实现error接口中的Error方法:func (e *MyconfigError) Error() string { return e.message }
当
MyconfigError.Error()
执行后将返回MyconfigError
中的错误信息,为string类型; -
MyconfigError使用
下面给出MyconfigError的一个简单使用的例子:
package main import ( "fmt" "user/readconfigutil" ) func main(){ var err readconfigutil.MyconfigError fmt.Println(err.Error()) }
将不输出任何东西。因为MyconfigError为它内部的message变量用空字符串进行初始化,所以err.Error()只会返回一个空字符串,所以输出空。而因为这里是在另一个包中import了readconfigutil包,而MyconfigError中的变量名不是以大写字母开头,所以我们不能在这个main函数中进行如:
var err = readconfigutil.MyconfigError{"a test error message", nil}
这样的赋值语句来为一个MyconfigError变量进行赋值或初始化,但是在readconfigutil包中,我们可以利用以下语句对MyconfigError进行初始化:
var err = MyconfigError{"a test error message", nil}
之所以这样,是因为在go语言中,一个包中对外可见的函数,类型定义,变量都必须以大写字母开头,否则就不允许在包外进行访问。因为MyconfigError的成员变量的名称均以小写字母开头,所以,不能在包外对这两个变量进行直接访问,否则将报错。上方的初始化是对两个变量的直接赋值,在包内可行,在包外被禁止。虽然在包外不能直接访问MyconfigError的两个变量,但是这两个变量的值可以通过MyconfigError的方法得到。
Listener相关
-
Listener接口定义:
type Listener interface { listen(filename string, c chan error) }
Listener接口存在一个用于监听的listen方法,所有实现了Listener接口的类型变量必须实现listen方法。
-
Listfunc自定义类型定义:
type ListenFunc func(string, chan error)
ListenFunc为我们自定义的所有函数签名形如
func(string, chan error)
的函数的类型的别名,是一个新的类型。ListenFunc类型实现了Listener接口:
func (t ListenFunc) listen(filename string, c chan error) { t(filename, c) }
可以看到,ListenFunc所实现的listen函数实际上是调用了ListenFunc函数,因此,ListenFunc才是listen函数的实际实现。
就原有的Listener接口而言,它只能接收所有实现了listen方法的类型变量,而不能接收一个函数,为了让Listener接口可以直接接收函数,我们自定义了ListenFunc类型,并让它实现Listener接口,那么我们在传值给Listener接口的时候,只需要将
func(string, chan error)
类型的函数进行强制类型转换,转换为ListenFunc类型变量,就可以传值给Listener接口,实现了函数直接传递给接口的想法; -
Listener接口使用
假设我们有如下的一
func(string, chan error)
签名的函数listentest,以及一个Watch函数:func listentest(filename string,c chan error){ fmt.Printf("this is a test for %s", filename) c<-nil } func Watch(filename string, Listener t){ c := make(chan error) t.listen(filename, c) anerror := <-c if anerror == nil{ fmt.Println("end") } }
那么我们可以像这样使用Listener接口:
package main import ( "fmt" "user/readconfigutil" ) func listentest(filename string,c chan error){ fmt.Printf("this is a test for %s", filename) c<-nil } func Watch(filename string, t readconfigutil.Listener){ c := make(chan error) go t.Listen(filename, c) anerror := <-c if anerror == nil{ fmt.Println(" end") } } func main(){ Watch("test.txt", readconfigutil.ListenFunc(listentest)) }
输出结果:
this is a test for test.txt end
可以看到,用户自定义的listentest函数可以作为一个变量传递给Listener接口,但是由于是ListenFunc类型实现了Listener接口,因此,我们需要将listentest函数进行以下的类型转换,使它成为一个实现了Listener类型的ListenFunc类型的变量:
// 在readconfigutil包中进行类型转换: ListenFunc(listentest) // 在readconfigutil包外进行类型转换: readconfigutil.ListenFunc(listentest)
在Watch函数中,我们
t.Listen
调用了listen函数,由ListFunc类型实现的listen方法可知,我们实际上调用函数为listentest。因此,只要我们的用户自定义了一个func(string, chan error)
类型的函数,就可以将它进行ListenFunc强制类型转换,作为Listener接口的传入值,传递给我们的Watch函数,这样,用户就能决定监听器的具体作用了。关于ListenFunc类型函数实现的几点要求:
-
因为本次实验需要使用到go程,所有需要保证Watch函数启动监听器的时候,主线程阻塞。为了让主线程阻塞,我们利用go中管道的阻塞特性,让主线程在listen函数返回管道之前都阻塞。所以,所有的listen在返回时必须使用如下的语句来启动主进程:
c<-nil
当然,管道也可以用于发送错误信息。
ps: 由于Listener中原有的listen方法不以大写字母开头,因此,我们在readconfigutil包外的main包中不能直接使用
t.listen(filename, c)
来使用listen函数,为了在main包中使用,我们临时在readconfig中将listen函数的名字改为Listen。当然,这里是为了给出一个示例供读者熟悉Listener的使用,因此临时改变Listener接口中listen方法的名字的首字母为大写,实际使用时还是以原来的定义为准 -
包实现
-
LoadResources:
func Loadresource(filename string) (config, error) { var torigin_file config stdin, err := os.Open(filename) if err != nil { return config{}, &MyconfigError{"Myerror: can't open file " + filename, err} } fin := bufio.NewReader(stdin) error_message = "" for true { data, enderror := fin.ReadString('\n') data = strings.TrimSpace(data) if data == "" { if enderror != nil { break } continue } if data[0] == note { continue } if data[0] == '[' { continue } terror_message := "" datas := strings.Split(data, "=") if len(datas) != 2 { terror_message = "Myerror: the argment of " + datas[0] terror_message = terror_message + " is too much or too least\n" error_message = error_message + terror_message continue } datas[0] = strings.TrimSpace(datas[0]) datas[1] = strings.TrimSpace(datas[1]) if datas[1] == "" { fmt.Printf("Warning: %s is empty\n", datas[0]) } if strings.Contains(datas[1], " "){ terror_message = "Myerror: " + datas[0] terror_message = terror_message + "'s value should not contain a space\n" } if datas[0] == "enforce_domain"{ _, changerror = strconv.ParseBool(datas[1]) if changerror != nil { terror_message = terror_message + "Myerror: enforce_domain's value you input is not a bool type\n" } } torigin_file.key = append(torigin_file.key, string(datas[0])) torigin_file.value = append(torigin_file.value, string(datas[1])) error_message = error_message + terror_message if enderror != nil { break } } stdin.Close() if error_message != ""{ return torigin_file, &MyconfigError{error_message, emptyerror} } return torigin_file, nil }
- Loadresource函数将接收被读入的文件名,并将其中的
key-value
对放入config结构中,最后返回; - Loadresource函数在返回一个config变量的时候,还会返回一个报错信息,如果这个error为nil说明加载完成,如果不为nil说明加载过程出现错误,使用Error()函数可以得到对应错误信息;
- Loadresource函数将接收被读入的文件名,并将其中的
-
listentest:
func listentest(filename string, c chan error) { for true { time.Sleep(100 * time.Millisecond) // 如果改动后存在语法错误,那么直接返回语法错误,不对其他改动进行检查 var temp config temp, synaxerror = Loadresource(filename) if synaxerror != nil { break } var check bool var exist []bool = make ([]bool, len(origin_file.key)) for i, v := range temp.key{ check = false for j, h := range origin_file.key{ if v == h{ check = true exist[j] = true if temp.value[i] != origin_file.value[j]{ change_message = change_message + "change: key " change_message = change_message + v change_message = change_message + " change to " change_message = change_message + temp.value[i] change_message = change_message + "\n" } break } } if check == false { change_message = change_message + "add: add a new line " change_message = change_message + v change_message = change_message + " with value " change_message = change_message + temp.value[i] change_message = change_message + "\n" } } for i, _ := range exist{ if exist[i] == false { change_message = change_message + "delete: delete a line " change_message = change_message + origin_file.key[i] change_message = change_message + " with value " change_message = change_message + origin_file.value[i] change_message = change_message + "\n" } } if change_message != "" { fmt.Print(change_message) origin_file = temp break } } c <- synaxerror close(c) }
listentest为我定义的一个监听器函数,具体的使用方法可以参照上方的Listener部分。listentest函数将不断地利用Loadresource函数加载文件,并将加载的新内容和原有的内容进行比较。当改动发生后,输出所有的改动信息并返回。如果在Loadresource部分发生错误,那么直接返回。
-
Watch:
func Watch(filename string, t Listener) (config, error) { origin_file, synaxerror = Loadresource(filename) if synaxerror != nil { return origin_file, synaxerror } c := make(chan error) go t.listen(filename, c) anerror := <-c return origin_file, anerror }
本次实验的Watch函数内容如上:Watch函数先使用Loadresource函数读取输入文件原有的配置,然后利用
go t.listen(filename, c)
启动监听器,Watch被阻塞,直到监听器结束监听。Watch将返回监听后的结果。
项目源码
https://gitee.com/wangyuwen2020/sever_count/tree/master/readconfigutil