基于Golang的监听&读取配置文件的程序包开发——simpleConfig_v1

一、配置文件概述

配置文件(Configuration File,CF)是一种文本文档,为计算机系统或程序配置参数和初始设置。传统的配置文件就是文本行,在 Unix 系统中随处可见,通常使用 .conf,.config,.cfg 作为后缀,并逐步形成了 key = value 的配置习惯。在 Windows 系统中添加了对 section 支持,通常用 .ini 作为后缀。面向对象语言的兴起,程序员需要直接将文本反序列化成内存对象作为配置,逐步提出了一些新的配置文件格式,包括 JSONYAMLTOML 等。

本次监听&读取配置文件的程序包()开发,主要应用于ini配置文件。
开发过程中使用的配置文件config.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



二、系统环境&项目介绍

1.系统环境

操作系统:CentOS7
硬件信息:使用virtual box配置虚拟机(内存3G、磁盘30G)
编程语言:GO 1.15.2

2.项目的任务要求

  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



三、具体程序设计及Golang代码实现

根据任务要求可知,simpleConfig_v1程序包中的watch函数相当于load、read、listen这三个函数的结合。在调用该函数时,首先会输出配置文件的原始信息,然后会一直监听配置文件有无改动,若有改动则会提示并展示最新的配置文件信息。

simpleConfig_v1程序包的函数架构如下:
在这里插入图片描述

下面按照simpleConfig_v1程序包的源码顺序来依次介绍数据结构和相关函数。

1. 数据结构

var sys string
var flag int

//three layer, like [server] -> protocol -> http
type Config [](map[string](map[string]string))

sys用于标记注释行, Unix 系统默认采用 # 作为注释行,Windows 系统默认采用 ; 作为注释行。
flag用于标记watch函数只能输出一次原始配置文件的信息。
Config是配置文件的数据结构,一个简单的配置文件最多可有三层,比如:

FirstLayer->SecondLayer->ThirdLayer
[server] -> protocol -> http



2. init函数模块

func init() {
	flag = 0
	if runtime.GOOS == "windows" {
		sys = ";"
	} else {
		sys = "#"
	}
}

使有 init 函数,使得 Unix 系统默认采用 # 作为注释行,Windows 系统默认采用 ; 作为注释行。


3.listen函数模块

type Listener interface {
	Listen(filename string)
}

type ListenFunc func(filename string) (Config, error)

func (fun ListenFunc) Listen(filename string) (Config, error) {
	return fun(filename)
}

func Watch(filename string, listener ListenFunc) (Config, error) {
	...
	//listen
	return listener.Listen(filename)
}

func main() {
	var listener ListenFunc = OnConfigChange
	filename := "config.ini"
	
	for {
		configuration, err := Watch(filename, listener)
		if err != nil {
			fmt.Println(err.Error())
		} else {
			fmt.Println(filename + "文件发生改变,改变后的配置信息如下:")
			//fmt.Println(configuration)
			for key, value := range configuration {
				fmt.Println(key, ":", value)
			}
			fmt.Println("")
		}

		time.Sleep(time.Duration(2) * time.Second)
	}
}

输入 listener 一个特殊的接口,用来监听配置文件是否被修改,让开发者自己决定如何处理配置变化。
- type ListenFunc func(string)
- type inteface Listener { listen(inifile string) }
- ListenFunc 实现接口方法 listen 直接调用函数
- 优点
- 所有满足签名的函数、方法都可以作为参数
- 所有实现 Listener 接口的数据类型都可作为参数

在上述例子中,listen函数的执行流程为

watch函数 -> listener.Listen(filename)函数 -> OnConfigChange函数

其中OnConfigChange函数具体实现如下:


func OnConfigChange(filename string) (Config, error) {
	temp := new(Config)
	config1, err := temp.ReadConfig(filename)
	if err != nil {
		return config1, err
	}

	flag2 := false
	for {
		temp2 := new(Config)
		config2, err := temp2.ReadConfig(filename)
		if err != nil {
			return config2, err
		}

		if len(config1) != len(config2) {
			return config2, nil
		}

		for _, i := range config2 {

			for j, k := range i {

				for l, m := range k {

					flag2 = false
					for _, n := range config1 {
						map1 := n[j]
						map2 := map1[l]
						if map2 == m {
							flag2 = true
						}
					}
					if flag2 == false {
						return config2, nil
					}
				}
			}
		}
	}
}

每当listen函数模块监听到配置文件由发生修改,便会提示修改信息和输出修改后的配置文件信息。


4.watch函数模块

//Watch = load + read + listen
func Watch(filename string, listener ListenFunc) (Config, error) {
	//load + read
	if flag == 0 {
		config := new(Config)
		configuration, err := config.ReadConfig(filename)
		if err != nil {
			fmt.Println(err.Error())
		} else {
			fmt.Println("")
			fmt.Println(filename + "文件原始的的配置信息如下:")
			//fmt.Println(configuration)
			for key, value := range configuration {
				fmt.Println(key, ":", value)
			}
			fmt.Println("")
		}
		flag = 1
	}

	//listen
	return listener.Listen(filename)
}

watch函数相当于load、read、listen这三个函数的结合。在调用该函数时,首先会输出配置文件的原始信息,然后会一直监听配置文件有无改动,若有改动则会提示并展示最新的配置文件信息。

其中load&read的函数为ReadConfig函数,其具体实现如下:

func (c *Config) ReadConfig(filename string) (Config, error) {

	file, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	var element map[string]map[string]string
	var FirstLayer string
	buf := bufio.NewReader(file)

	for {

		l, err := buf.ReadString('\n')
		line := strings.TrimSpace(l)
		if err != nil {
			if err != io.EOF {
				return nil, err
			}
			if len(line) == 0 {
				break
			}
		}

		switch {

		case len(line) == 0:
		case string(line[0]) == sys:

		case line[0] == '[' && line[len(line)-1] == ']':
			FirstLayer = strings.TrimSpace(line[1 : len(line)-1])
			element = make(map[string]map[string]string)
			element[FirstLayer] = make(map[string]string)

		default:
			index := strings.IndexAny(line, "=")
			value := strings.TrimSpace(line[index+1 : len(line)])
			if FirstLayer == "" {
				FirstLayer = "FirstLayer"
			}
			element = make(map[string]map[string]string)
			element[FirstLayer] = make(map[string]string)
			valmap := strings.TrimSpace(line[0:index])
			element[FirstLayer][valmap] = value
			*c = append(*c, element)
		}
	}

	return *c, nil
}



四、设置自定义错误

利用errors包,可以在【三】的基础上添加自定义错误如下:

func OnConfigChange(filename string) (Config, error) {
	temp := new(Config)
	config1, err := temp.ReadConfig(filename)
	if err != nil {
		err2 := errors.New("Could not read the config file.")
		return config1, err2
	}
	...
			temp2 := new(Config)
			config2, err := temp2.ReadConfig(filename)
			if err != nil {
				err2 := errors.New("Could not read the config file.")
				return config2, err2
			}
			...
}

func (c *Config) ReadConfig(filename string) (Config, error) {
	file, err := os.Open(filename)
	if err != nil {
		err2 := errors.New("Could not open the config file.")
		return nil, err2
	}
	...
			l, err := buf.ReadString('\n')
			line := strings.TrimSpace(l)
			if err != nil {
				if err != io.EOF {
					err2 := errors.New("Could not read the config element.")
					return nil, err2
				}
				if len(line) == 0 {
					break
				}
			}
	...
}



五、程序测试

1.封装并使用程序包

将项目simpleConfig_v1的simpleConfig_v1.go文件的main函数注释掉,package改为package simpleConfig_v1,然后执行如下指令:

go build

在其他路径下建立main.go,内容如下(listen函数可由用户自定义设置,此处使用simpleConfig_v1自带的OnConfigChange函数):

//main.go
package main

import (
	"fmt"
	"time"

	"github.com/user/simpleConfig_v1"
)

func main() {
	var listener simpleConfig_v1.ListenFunc = simpleConfig_v1.OnConfigChange
	filename := "config.ini"

	for {
		configuration, err := simpleConfig_v1.Watch(filename, listener)
		if err != nil {
			fmt.Println(err.Error())
		} else {
			fmt.Println(filename + "文件发生改变,改变后的配置信息如下:")
			//fmt.Println(configuration)
			for key, value := range configuration {
				fmt.Println(key, ":", value)
			}
			fmt.Println("")
		}

		time.Sleep(time.Duration(2) * time.Second)
	}

}

并在main.go的目录下存放config.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



2.功能测试

功能测试主要从用户角度测试程序包的功能,步骤如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

[henryhzy@localhost user]$ go run main.go

config.ini文件原始的的配置信息如下:
0 : map[FirstLayer:map[app_mode:development]]
1 : map[paths:map[data:/home/git/grafana]]
2 : map[server:map[protocol:http]]
3 : map[server:map[http_port:9999]]
4 : map[server:map[enforce_domain:true]]

config.ini文件发生改变,改变后的配置信息如下:
0 : map[FirstLayer:map[app_mode:development]]
1 : map[paths:map[data:/home/git/grafana]]
2 : map[server:map[protocol:http]]
3 : map[server:map[http_port:18342026]]
4 : map[server:map[enforce_domain:true]]

config.ini文件发生改变,改变后的配置信息如下:
0 : map[FirstLayer:map[app_mode:henryhzy]]
1 : map[paths:map[data:/home/git/grafana]]
2 : map[server:map[protocol:http]]
3 : map[server:map[http_port:18342026]]
4 : map[server:map[enforce_domain:true]]

^Csignal: interrupt

由此可知程序包的功能测试正常,调用程序包后首先会输出配置文件的原始信息,然后会一直监听配置文件有无改动,若有改动则会提示并展示最新的配置文件信息。通过键盘ctrl+c可以终止监听程序。


3.单元测试

单元测试主要从程序员角度,对程序包的具体函数进行测试。

①init函数
测试代码:

func Test_init(t *testing.T) {
	var test_sys string = "#"

	got := sys
	want := test_sys

	if got != want {
		t.Errorf("\n got %s\n want %s\n", got, want)
	}
}

测试结果:
在这里插入图片描述

②ReadConfig函数
测试代码:

func Test_ReadConfig(t *testing.T) {
	filename := "config.ini"

	temp := new(Config)
	_, err := temp.ReadConfig(filename)
	if err != nil {
		t.Errorf("ReadConfig function failed\n%s\n", err)
	}
}

测试结果:
在这里插入图片描述

③listen函数
测试代码:

func Test_listen(t *testing.T) {
	var listener ListenFunc = OnConfigChange
	filename := "error_name.ini"

	_, err := listener(filename)
	got := fmt.Sprintf("%s", err)

	err2 := errors.New("Could not read the config file.")
	want := fmt.Sprintf("%s", err2)

	if got != want {
		t.Errorf("\nListen function failed\n%s\n%s", err, err2)
	}
}

测试结果:
在这里插入图片描述
④watch函数
测试代码:

func Test_watch(t *testing.T) {
	var listener ListenFunc = OnConfigChange
	filename := "error_name.ini"

	_, err := Watch(filename, listener)
	got := fmt.Sprintf("%s", err)

	err2 := errors.New("Could not read the config file.")
	want := fmt.Sprintf("%s", err2)

	if got != want {
		t.Errorf("\nListen function failed\n%s\n%s", err, err2)
	}
}

测试结果:
在这里插入图片描述
通过简单的单元测试可知,程序包的函数均可正常调用。


六、中文 api 文档

首先安装godoc如下:

git clone https://github.com/golang/tools $GOPATH/src/golang.org/x/tools
go build golang.org/x/tools

将项目simpleConfig_v1的simpleConfig_v1.go文件的main函数注释掉,package改为package simpleConfig_v1,然后执行如下指令:

go install
go doc
godoc -url="pkg/github.com/user/simpleConfig_v1" > API.html

便会在当前目录下生成API.html文件:
在这里插入图片描述



七、完整代码

具体代码可见gitee仓库:gitee


八、References

  1. ini 读写包。 Github
  2. Viper 读配置集成解决方案包。Github
  3. fsnotify 文件系统通知包。 Github
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值