Service computing (4)-package development, read simple configuration file v1

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, .cfgas a suffix, and gradually formed a key = valueconfiguration habits. Added sectionsupport for Windows systems , usually used .inias 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

  1. Familiar with programming habits (idioms) and styles (convetions)
  2. Familiar with io library operation
  3. Use a test-driven approach
  4. Simple Go program use
  5. 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

Insert picture description here

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 errorsunder 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, Initwith 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

Insert picture description 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.gois 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/readiniinstructions 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.

Insert picture description here

Since then, the function is almost achieved.

Generate API documentation

First go get golang.org/x/tools/cmd/godocinstall 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=onand in turn export GOPROXY=https://goproxy.cn, and then execute the go get (best not When written into the ~/.profilefile, 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 godocit 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/)

Insert picture description here

Finally, if you save the document, it is an instruction godoc -url "http://localhost:6060/pkg/github.com/github-user/test/readini/" > api.html.

3. Assignment submission

My complete code

Guess you like

Origin blog.csdn.net/qq_43278234/article/details/109143078