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

服务计算第四次作业

任务目标

  • 熟悉程序包的编写习惯(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()函数可以得到对应错误信息;
  • 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值