Article Directory
1 Overview
The configuration file (Configuration File, CF) is a text file that configures parameters and initial settings for a computer system or program. The traditional configuration file is a text line, everywhere in Unix systems typically use .conf
, .config
, .cfg
as a suffix, and gradually formed a key = value
configuration habits. Added section
support for Windows systems , usually used .ini
as a suffix. With the rise of object-oriented languages, programmers need to directly deserialize text into memory objects as configuration, and gradually propose some new configuration file formats, including JSON, YAML, TOML, etc.
2. Course tasks
mission target
- Familiar with programming habits (idioms) and styles (convetions)
- Familiar with io library operation
- Use a test-driven approach
- Simple Go program use
- Event notification
Task content
Publish a configuration file reading package on Gitee or GitHub. The first version only needs to read the ini configuration. Examples of configuration file format:
# 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
Task requirements
ini read and write package , Viper read configuration integration solution package , fsnotify file system notification package
mission accomplished
Create a new package readini, which implements functions Watch(filename,listener) (configuration, error)
.
First, we must design the corresponding data structure. According to the task requirements, we can design it like this.
// Section下面的键值对
type Element map[string]string
// ini文件结构(对象)
// Object为各个Section所对应的所有键值对
type Configuration map[string][]Element
// Listener接口
type Listener interface {
Listen(inifile string)
}
At the same time, Unix systems use it #
as a comment line by default, and Windows systems use it ;
as a comment line by default . Therefore, an init function is needed to initialize the comment symbol of the current system.
// 当前系统下的注释符
var CommentSymbol byte
// 通过确定当前操作系统,从而确定相应的注释符
func Init() {
sysType := runtime.GOOS
if sysType == "linux" {
CommentSymbol = '#'
}
if sysType == "windows" {
CommentSymbol = ';'
}
}
Then you can design the Watch function.
// 监听自函数运行以来发生的一次配置文件变化并返回最新的配置文件解析内容。
func Watch(filename string, listener Listener) (Configuration, error) {
listener.Listen(filename)
i := make(Configuration)
var e error = nil
f, err := os.Open(filename)
if err != nil {
e = errors.New("Open file faild.")
return i, e
}
defer f.Close()
// 将ini文件转换成一个bufio
r := bufio.NewReader(f)
// 当前所解析到的section
section := ""
for {
// 以'\n'作为结束符读入一行
line, err := r.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
e = errors.New("Read file faild.")
break
}
// 删除行两端的空白字符
line = strings.TrimSpace(line)
// 解析一行中的内容
// 空行则跳过
if line == "" {
continue
}
// 以符号CommentSymbol作为注释
if line[0] == CommentSymbol {
continue
}
length := len(line)
// 匹配字符串
if line[0] == '[' && line[length-1] == ']' {
// section
section = line[1 : length-1]
// 如果map中没有这个section,添加进来
if _, ok := i[section]; !ok {
i[section] = []Element{
}
}
}else {
// 键值对数据
// 分割字符串
s := strings.Split(line, "=")
if len(s) < 2 {
e = errors.New("Incorrect key-value pair format.")
break
}
key := strings.TrimSpace(s[0])
value := strings.TrimSpace(s[1])
element := make(Element)
element[key] = value
// 把键值对添加进section里面
if section == "" {
i[section] = []Element{
}
}
if _, ok := i[section]; ok {
i[section] = append(i[section], element)
}
}
}
return i, e
}
First, listen to the changes in the configuration file through the listener (my follow-up test uses the spin lock method). Once the change is detected, it immediately jumps out of the spin lock, parses the section key-value pair, and finally returns the parsed content Configuration and error error. At the same time, the custom error error uses the function errors
under the library New(string)
to determine the corresponding error message.
Since then, a simple readini package has been completed, which provides a function Watch(filename,listener) (configuration, error)
.
Finally execute the instruction go build github.com/github-user/test/readini
, and other programs can import "github.com/github-user/test/readini"
import this package in a way.
test
readini bag, it was only two functions, Init
with Watch
, respectively, to write a test function for two functions.
func ExampleInit() {
Init()
fmt.Printf("%c\n",CommentSymbol)
//Output:#
}
type ListenFunc func(string)
func (l ListenFunc) Listen(inifile string) {
l(string(inifile))
}
func ExampleWatch() {
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()) {
break
}
time.Sleep(time.Duration(1)*time.Second)
}
}
go func(){
conf, err := Watch("example.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()
}
}()
file,openErr:=os.Create("example.ini")
defer os.Remove("example.ini")
defer file.Close()
if openErr!=nil {
panic(openErr)
}
time.Sleep(time.Duration(1)*time.Second)
writer:=bufio.NewWriter(file)
_,errWrite := writer.Write([]byte("[test]\n"))
_,errWrite = writer.Write([]byte("value1 = 123\n"))
_,errWrite = writer.Write([]byte("value2 = 222\n"))
if errWrite!=nil {
os.Exit(0)
}
writer.Flush()
time.Sleep(time.Duration(2)*time.Second)
/*Output:
Section: test
Key: value1 Value: 123
Key: value2 Value: 222
*/
}
In ExampleWatch, the main idea is to execute the Watch function through a go program and print the returned Configuration. The reason for using the go program is because listen is an implementation of a spin lock. If you need to update the file outside to trigger it, you can't block the main thread.
The most important thing to pay attention to here is the use of the delay function to ensure the order of execution of the statements and avoid the subsequent file write operations from being executed too fast, making the update undetectable in the go program. I also suffered a bit here
After unit testing, it should be the turn of integration testing. But because I only have two functions here, and the Init function is a bit simpler than the Watch function. Even the post-integration test is not much different from the test of the separate Watch function, so I am not writing post-integration here. In the follow-up functional test, you can actually see the effect after the integration.
Because what we want to design here is a readini package, since it is a package, it should be able to be imported in other packages through import.
Therefore, an additional one main.go
is created to test the call to this package function.
package main
// 主函数
import (
"fmt"
"os"
"time"
"bufio"
"github.com/github-user/test/readini"
)
type ListenFunc func(string)
func (l ListenFunc) Listen(inifile string) {
l(inifile)
}
func main() {
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("There are something changed in file ", inifile)
break
}
fmt.Println("Listening changes in file ", inifile)
time.Sleep(time.Duration(1)*time.Second)
}
}
go func(){
readini.Init()
for {
conf, err := readini.Watch("../111.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(3)*time.Second)
i := 0
file,openErr := os.OpenFile("../111.ini", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if openErr != nil {
panic(openErr)
}
writer := bufio.NewWriter(file)
_, errWrite := writer.Write([]byte("[test]\n"))
if errWrite != nil {
panic(errWrite)
}
writer.Flush()
for {
_, errWrite = writer.Write([]byte(fmt.Sprintf("Test_key%d = Test_value%d\n", i, i)))
i += 1
if errWrite != nil {
panic(errWrite)
}
writer.Flush()
time.Sleep(time.Duration(3)*time.Second)
}
}
After executing the go build github.com/github-user/test/readini
instructions before, the program still runs without problems.
Similar to the test of the Watch function, here we create a go program. After Init is initialized, the Watch function is executed continuously, and the latest analysis content is output once the configuration file changes are monitored.
At the same time, in the main thread, a new key-value pair is written every 3 seconds. Therefore, after executing the instruction go run main.go
, what we can see is that a change in the configuration file is detected about every 3 seconds and the latest output is output. Parse the content.
Since then, the function is almost achieved.
Generate API documentation
First go get golang.org/x/tools/cmd/godoc
install godoc through instructions . Because this command is downloaded from the official website, it is very likely that the access will time out. At this time, you can first connect to the Go module agent in China , that is, execute the command export GO111MODULE=on
and in turn export GOPROXY=https://goproxy.cn
, and then execute the go get (best not When written into the ~/.profile
file, the Go module that is not continued may suddenly have a series of indescribable errors, I have stepped on the pit ).
Then execute the command go build golang.org/x/tools/cmd/godoc
, and then execute godoc
it and you can access it in the browser through http://localhost:6060/, just find the corresponding path. (My words are http://localhost:6060/pkg/github.com/github-user/test/readini/)
Finally, if you save the document, it is an instruction godoc -url "http://localhost:6060/pkg/github.com/github-user/test/readini/" > api.html
.