Golang程序包开发,读简单配置文件 v1
概述
配置文件(Configuration File,CF)是一种文本文档,为计算机系统或程序配置参数和初始设置。传统的配置文件就是文本行,在 Unix 系统中随处可见,通常使用 .conf,.config,.cfg 作为后缀,并逐步形成了 key = value 的配置习惯。在 Windows 系统中添加了对 section 支持,通常用 .ini 作为后缀。面向对象语言的兴起,程序员需要直接将文本反序列化成内存对象作为配置,逐步提出了一些新的配置文件格式,包括 JSON,YAML,TOML 等。
本次实验需要用golang实现一个非常简单的读配置文件程序包。
任务要求
包必须提供一个函数 Watch(filename,listener) (configuration, error)
输入 filename 是配置文件名
输入 listener 一个特殊的接口,用来监听配置文件是否被修改,让开发者自己决定如何处理配置变化
输出 configuration 数据类型,可根据 key 读对应的 value。 key 和 value 都是字符串
输出 error 是错误数据,如配置文件不存在,无法打开等
一、设计说明
新建一个名为ini的包,项目结构如下
Watch(filename,listener) (configuration, error)等主要函数在ini.go文件中实现。
inisys.go中主要是包含了一个ini()函数,其可以判断当前系统是linux系统还是windows系统。
观察Watch(filename,listener) (configuration, error)的参数和返回值,filename和error都比较容易理解,据要求所说,listener是一个可以监听配置文件是否被修改的接口。那么configuration数据类型是啥呢,通过查看INI中的例子,发现configuration数据结构中还应该包含一个名为section的数据结构,而section在configuration中可以用map来存储。而在section数据结构中利用一个map来存储键值对。如下所示:
type configuration struct{
sections map[string]section
}
type section struct{
m map[string]string
}
为了能够通过configuration.Section(“XXX”).Key(“XXX”)来得到对应的值,还需要在configuration和section中分别定义一个方法。
func (cfg configuration) Section(str string) section {
temp := cfg.sections[str]
return temp
}
func (s section) Key(k string) string {
v := s.m[k]
return v
}
定义好基础结构后,接下来就是对输入文件进行处理,主要思路如下:
- 逐行读取文件
- 遇到开头为‘#’的行,表示注释,跳过
- 若键值对在[XXX]下,则表示该键值对处于分区“XXX”中。若没有给出[XXX]则默认在分区“”(空字符串)中。
- 等号左边的字符串表示键,等号右边的字符串表示值。若一行中有两个等号,则返回一个自定义错误(等号太多)。
func Load(path string,ret_cfg chan configuration,ret_err chan error)/*(configuration,error)*/{
var cfg configuration
var err error
cfg.sections = make(map[string]section)
f, err := os.Open(path)
defer f.Close()
if err != nil {
//panic(err)
flag = true
ret_cfg <- cfg
ret_err <- err
return
}
r := bufio.NewReader(f)
k := ""
var v section
v.m = make(map[string]string)
sys := ini()
for {
b, _, err2 := r.ReadLine()
err = err2
//time.Sleep(500*1000*1000)
if err != nil {
if err == io.EOF {
err = nil
break
}
panic(err)
}
s := strings.TrimSpace(string(b))
if len(s) <=0 {
continue
}
if sys == true && s[0]=='#'{
continue
}
if sys == false && s[0]==';'{
continue
}
/*if s[0]=='#'||s[0]==';'{
continue
}*/
if strings.Count(s,"=") > 1{
err = errors.New("Too many = ")
break
}
index := strings.Index(s, "=")
if index < 0 {
if s[0]=='['&&s[len(s)-1]==']' {
cfg.sections[k] = v
v.m = make(map[string]string)
k = s[1:len(s)-1]
}
continue
}
key := strings.TrimSpace(s[:index])
if len(key) == 0 {
continue
}
value := strings.TrimSpace(s[index+1:])
if len(value) == 0 {
continue
}
v.m[key] = value
}
cfg.sections[k] = v
flag = true
ret_cfg <- cfg
ret_err <- err
}
有了对文件输入的处理,接下来就是对listener接口的实现,由于其可以监听文件是否被修改,因此,该接口中应当有一个listen方法,其可以对文件状态进行监听。
type Listener interface {
listen(string,chan int)
}
如何判断文件是否被修改了呢?golang在time包提供了ModTime().Unix()函数,通过它可以获取文件最后一次被修改的时间,那么,只需在一开始记录下文件的修改时间,然后每隔一段时间,再次调用该函数,比较二者的值是否相同即可。
func GetFileModTime(path string) int64{
f, err := os.Open(path)
if err != nil {
return -1
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return -1
}
return fi.ModTime().Unix()
}
func (t T)listen(path string,c chan int){
var num1,num2 int64
num1 = GetFileModTime(path)
for {
time.Sleep(100*1000*1000)
num2 = GetFileModTime(path)
if num1 != num2 {
num1 = num2
fmt.Println("File has been changed")
c <- -1
break
}
if flag == true{
c <- 0
break
}
}
}
到这里,对文件输入的处理(Load函数)和对文件的监听(listen函数)都已经完成了,万事俱备,就差最后的Watch函数的实现了。Watch函数实现的大致思路如下:
- 首先启动Load函数,对文件输入进行处理,启动listen函数对文件状态进行监听
- 若文件被修改,则listener通过信道通知Watch,重新开启一个Load函数,对修改过后的文件进行重新读取。
func Watch(path string,l Listener)(configuration,error){
var cfg configuration
var err error
ret_cfg := make(chan configuration)
ret_err := make(chan error)
c := make(chan int)
go Load(path,ret_cfg,ret_err)
go l.listen(path,c)
x := <-c
if x == -1 {
go Load(path,ret_cfg,ret_err)
cfg = <-ret_cfg
err = <-ret_err
}
cfg = <-ret_cfg
err = <-ret_err
return cfg,err
}
二、功能测试
首先安装包
go install github.com/github-user/ini
然后新建一个main.go文件
package main
import "fmt"
import "os"
import ini "github.com/github-user/ini"
func main() {
var l ini.Listener
l = ini.T{}
cfg, err := ini.Watch("my.ini",l)
if err != nil {
fmt.Printf("Fail to read file: %v", err)
os.Exit(1)
}
fmt.Println("App Mode:", cfg.Section("").Key("app_mode"))
fmt.Println("Data Path:", cfg.Section("paths").Key("data"))
fmt.Println("Server Protocol:", cfg.Section("server").Key("protocol"))
fmt.Println("Port Number:", cfg.Section("server").Key("http_port"))
fmt.Println("Enforce Domain:", cfg.Section("server").Key("enforce_domain"))
}
输入文件my.ini内容如下:
# possible values : production, development
app_mode = development
[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
data = /home/git/grafana
[server]
# Protocol (http or https)
protocol = http
# The http port to use
http_port = 9999
# Redirect to correct domain if host header does not match domain
# Prevents DNS rebinding attacks
enforce_domain = true
执行
go run main.go
测试一下中途对文件进行修改,由于不加以限制的话,文件的读取很快就完成了,因此为了在运行时可以修改文件,Load函数中每读取一行数据,程序都会sleep0.5秒,当listener监听到文件被修改时,会在屏幕上输出,File has been changed,并且再次运行Load函数。
自定义错误:
若输入文件中有一行的键值对出现了两个以上的等号,则停止输入,并返回一个自定义错误
三、单元测试
在ini包下的ini_test.go编写测试文件,由于关键部分是对文件输入的处理,因此主要测试Load函数执行所得到值是否与期望得到的值相同。
package ini
import "testing"
func TestIni(t *testing.T){
var cfg configuration
var err error
ret_cfg := make(chan configuration)
ret_err := make(chan error)
go Load("my.ini",ret_cfg,ret_err)
cfg = <-ret_cfg
err = <-ret_err
var str1 [5]string
var str2 [5]string
str1[0] = "development"
str1[1] = "/home/git/grafana"
str1[2] = "http"
str1[3] = "9999"
str1[4] = "true"
str2[0] = cfg.Section("").Key("app_mode")
str2[1] = cfg.Section("paths").Key("data")
str2[2] = cfg.Section("server").Key("protocol")
str2[3] = cfg.Section("server").Key("http_port")
str2[4] = cfg.Section("server").Key("enforce_domain")
for i:=0;i<5;i++ {
if str1[i] != str2[i]{
t.Errorf("expected '%s' but got '%s'",str1[i],str2[i])
}
}
if err != nil {
t.Error(err)
}
}
除此之外,还需要编写一个inisys_test.go文件,用来对ini()函数做测试
package ini
import "testing"
func TestInisys(t *testing.T){
if ini() != true {
t.Errorf("The program run in windows")
}
}
执行
go test