Golang程序包开发,读简单配置文件 v1

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

在这里插入图片描述

项目链接

gitee地址

参考链接

开始使用INI
golang 获取文件修改时间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值