中山大学服务计算第四次作业

it2025-01-20  16

服务计算第四次作业

任务目标

熟悉程序包的编写习惯(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 读配置集成解决方案包。Githublive 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()函数可以得到对应错误信息;

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

最新回复(0)