我们在日常工作中经常会更新配置文件,相对来说配置文件比较简单,常见的有 “key = value”的模式,以下两个例子,均是实现key/value的更新。
第一个:更新简单的key/value配置文件。
针对类似如下的配置文件:
# Sample Conf
# default log file
log = /var/log/sample.log
# pid file
pid-file = /var/run/sample.pid
函数:updateConfigFileA
参数:
- filePath - 配置文件全路径
- args - 允许同时更新多个 key/value pair,通过map的形式传入
这里使用了 io/ioutil 提供的封装函数读写文件,非常方便。
ioutil.ReadFile 将会一次性的读入所有数据,如果源文件不大的话,没有任何问题,反过来源文件太大的话,将占用很大的内存,特别是并发环境下读取的话,占用的内存就非常巨大了。这时候可以考虑用 io.Copy,它 copy 数据从一个 Reader接口,到Writer接口。io.Copy 使用固定的32KB buffer 来copy 数据,无论源文件多少,每次都是固定的32KB,不会大量的占用内存。我们可以定制专门的 Reader 和 Writer 来处理 copy 的数据。
大部分场景下,配置文件都不是很大,基本可以忽略 ioutil.ReadFile 的性能问题。
具体代码如下,
func updateConfigFileA(filePath string, args map[string]string) error {
input, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
lines := strings.Split(string(input), "\n")
for k, v := range args {
if len(k) == 0 {
continue
}
hit := false
for i, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), k) {
lines[i] = fmt.Sprintf("%s = %s", k, v)
hit = true
}
}
if !hit {
lines = append(lines, fmt.Sprintf("%s = %s", k, v))
}
}
output := strings.Join(lines, "\n")
if err = ioutil.WriteFile(filePath, []byte(output), 0644); err != nil {
return err
}
return nil
}
ioutil.WriteFile() 接受三个参数
- 待写入的文件
- 待写入的数据
- 待写入文件的 Permission mode
如果待写入的文件不存在的话,才用到这个参数,用其生成新文件
第二个:更新带有分段的key/value配置文件。
针对类似如下的配置文件:
# Sample Conf
[System]
# default log file
log = /var/log/sample.log
# pid file
pid-file = /var/run/sample.pid
[Home]
address = "City|Street|Home-Block" # this is address
# this is phone number
phone = 1234567890
# this is email
email = sample1@sample.com
email = sample2@sample.com
email = sample3@sample.com
函数:updateConfigFileB
参数:
- filePath - 配置文件全路径
- section - 段标识,例如System/Home,不能为空
- key/value - 键值对,不能为空
- op - 操作码,有三个取值,
- 0:更新键值对,针对key唯一的场景,更新其value,例如 Home中 addres 和 phone 字段。
- 1:更新键值对,针对key不唯一的场景,增加键值对,例如 Home 中的 email 字段。
- -1:删除键所对应的字段。(可以改代码变成删除键值对)
- 0:更新键值对,针对key唯一的场景,更新其value,例如 Home中 addres 和 phone 字段。
样例:
删除 System 中的 log 字段
updateConfigFileB(“pathToFile”, “System”, “log”, “”, -1)更新 Home 中的 phone 字段
updateConfigFileB(“pathToFile”, “home”, “phone”, “0987654321”, 0)更新 Home 中的 email 字段,添加一个新的键值对
updateConfigFileB(“pathToFile”, “Home”, “email”, “sample4@sample.com”, 1)增加新的分段 Office,及其字段 address
updateConfigFileB(“pathToFile”, “Office”, “address”, “City|Street|Office-Block”, 0)
具体代码如下,
func updateConfigFileC(filePath string, section, key, value string, op int) error {
if len(section) == 0 || len(key) == 0 {
return errors.New("any of section and key cannot be none")
}
section = strings.TrimSpace(section)
key = strings.TrimSpace(key)
file, err := os.OpenFile(filePath, os.O_RDWR, 0644)
if err != nil {
return err
}
defer file.Close()
var (
buffer bytes.Buffer
writeBufferToFile = false
hasSection = false
scanner = bufio.NewScanner(file)
preWhiteSpaces = strings.Repeat(" ", 4) // how many white spaces left
kvLine = fmt.Sprintf("%s%s = %s", preWhiteSpaces, key, value)
patSection = fmt.Sprintf("^\\s*\\[\\s*(?i:%s)\\s*\\]\\s*#?", section)
patKV = fmt.Sprintf("(^\\s*%s\\s*=)\\s*(\\S[^#]*)", key)
regKV = regexp.MustCompile(patKV)
regSection = regexp.MustCompile(`^\s*\[\s*(\S.*)\s*\]\s*#?`)
)
handler := func(orgLine string) ([]string, bool) {
var (
opUpd = false
hasUpd = false
isNextSection = false
lines = []string{orgLine}
)
for scanner.Scan() {
line := scanner.Text()
smSection := regSection.FindStringSubmatch(line)
if len(smSection) > 1 && !strings.EqualFold(smSection[1], section) {
isNextSection = true
break
}
switch op {
case -1: // delete the pair of key and value
if regKV.FindString(line) != "" {
hasUpd = true
continue
}
case 0: // update
smKV := regKV.FindStringSubmatch(line)
if len(smKV) > 2 {
opUpd = true
if strings.TrimSpace(smKV[2]) != strings.TrimSpace(value) {
rLine := regKV.ReplaceAllString(line, "$1 "+value+" ")
lines = append(lines, rLine)
hasUpd = true
continue
}
}
case 1: // multiple keys
smKV := regKV.FindStringSubmatch(line)
if len(smKV) > 2 {
if strings.TrimSpace(smKV[2]) == strings.TrimSpace(value) {
opUpd = true
}
}
}
lines = append(lines, line)
}
if !opUpd && op != -1 {
lines = append(lines, kvLine)
for i := len(lines) - 2; i > 0; i-- {
if isNull, _ := regexp.MatchString("^\\s*$", lines[i]); isNull {
lines[i], lines[i+1] = lines[i+1], lines[i]
continue
}
break
}
hasUpd = true
}
if isNextSection {
lines = append(lines, scanner.Text())
}
return lines, hasUpd
}
for scanner.Scan() {
orgLine := scanner.Text()
matched, _ := regexp.MatchString(patSection, orgLine)
if matched {
var updLines []string
updLines, writeBufferToFile = handler(orgLine)
if updLines != nil && writeBufferToFile {
for i := range updLines {
buffer.WriteString(updLines[i] + "\n")
}
}
hasSection = true
continue
}
buffer.WriteString(orgLine + "\n")
}
if !hasSection && op != -1 {
buffer.WriteString("\n[" + section + "]\n")
buffer.WriteString(kvLine + "\n")
writeBufferToFile = true
}
if writeBufferToFile {
if _, err := file.Seek(0, 0); err != nil {
return err
}
if err := file.Truncate(0); err != nil {
return err
}
if _, err := file.Write(buffer.Bytes()); err != nil {
return err
}
}
return nil
}
参考链接: