服务计算 作业四 读简单配置文件——设计文档

设计要求

  • 核心任务:包必须提供一个函数 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 系统默认采用 ; 作为注释行。
  • 不能使用第三方包,但可以参考、甚至复制它们的代码。例如:

    • ini 读写包。 Github中文支持
    • Viper 读配置集成解决方案包。Github
      live watching and re-reading of config files (optional)
    • fsnotify 文件系统通知包。 Github
    • 你可以参考这些代码,但不能在你的包中 import

设计说明

根据给出的实现要求,我们的读取配置文件包主要就是这两个部分:

  • 第一,实现接口调用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文档为:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值