我们在日常工作中经常会更新配置文件,相对来说配置文件比较简单,常见的有 “key = value”的模式,以下两个例子,均是实现key/value的更新。

第一个:更新简单的key/value配置文件。

针对类似如下的配置文件:

  1. # Sample Conf
  2. # default log file
  3. log = /var/log/sample.log
  4. # pid file
  5. 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 的性能问题。

具体代码如下,

  1. func updateConfigFileA(filePath string, args map[string]string) error {
  2. input, err := ioutil.ReadFile(filePath)
  3. if err != nil {
  4. return err
  5. }
  6. lines := strings.Split(string(input), "\n")
  7. for k, v := range args {
  8. if len(k) == 0 {
  9. continue
  10. }
  11. hit := false
  12. for i, line := range lines {
  13. if strings.HasPrefix(strings.TrimSpace(line), k) {
  14. lines[i] = fmt.Sprintf("%s = %s", k, v)
  15. hit = true
  16. }
  17. }
  18. if !hit {
  19. lines = append(lines, fmt.Sprintf("%s = %s", k, v))
  20. }
  21. }
  22. output := strings.Join(lines, "\n")
  23. if err = ioutil.WriteFile(filePath, []byte(output), 0644); err != nil {
  24. return err
  25. }
  26. return nil
  27. }

ioutil.WriteFile() 接受三个参数

  • 待写入的文件
  • 待写入的数据
  • 待写入文件的 Permission mode
    如果待写入的文件不存在的话,才用到这个参数,用其生成新文件

第二个:更新带有分段的key/value配置文件。

针对类似如下的配置文件:

  1. # Sample Conf
  2. [System]
  3. # default log file
  4. log = /var/log/sample.log
  5. # pid file
  6. pid-file = /var/run/sample.pid
  7. [Home]
  8. address = "City|Street|Home-Block" # this is address
  9. # this is phone number
  10. phone = 1234567890
  11. # this is email
  12. email = sample1@sample.com
  13. email = sample2@sample.com
  14. email = sample3@sample.com

函数:updateConfigFileB
参数:

  • filePath - 配置文件全路径
  • section - 段标识,例如System/Home,不能为空
  • key/value - 键值对,不能为空
  • op - 操作码,有三个取值,
    • 0:更新键值对,针对key唯一的场景,更新其value,例如 Home中 addres 和 phone 字段。
    • 1:更新键值对,针对key不唯一的场景,增加键值对,例如 Home 中的 email 字段。
    • -1:删除键所对应的字段。(可以改代码变成删除键值对)

样例:

  1. 删除 System 中的 log 字段
    updateConfigFileB(“pathToFile”, “System”, “log”, “”, -1)

  2. 更新 Home 中的 phone 字段
    updateConfigFileB(“pathToFile”, “home”, “phone”, “0987654321”, 0)

  3. 更新 Home 中的 email 字段,添加一个新的键值对
    updateConfigFileB(“pathToFile”, “Home”, “email”, “sample4@sample.com”, 1)

  4. 增加新的分段 Office,及其字段 address
    updateConfigFileB(“pathToFile”, “Office”, “address”, “City|Street|Office-Block”, 0)

具体代码如下,

  1. func updateConfigFileC(filePath string, section, key, value string, op int) error {
  2. if len(section) == 0 || len(key) == 0 {
  3. return errors.New("any of section and key cannot be none")
  4. }
  5. section = strings.TrimSpace(section)
  6. key = strings.TrimSpace(key)
  7. file, err := os.OpenFile(filePath, os.O_RDWR, 0644)
  8. if err != nil {
  9. return err
  10. }
  11. defer file.Close()
  12. var (
  13. buffer bytes.Buffer
  14. writeBufferToFile = false
  15. hasSection = false
  16. scanner = bufio.NewScanner(file)
  17. preWhiteSpaces = strings.Repeat(" ", 4) // how many white spaces left
  18. kvLine = fmt.Sprintf("%s%s = %s", preWhiteSpaces, key, value)
  19. patSection = fmt.Sprintf("^\\s*\\[\\s*(?i:%s)\\s*\\]\\s*#?", section)
  20. patKV = fmt.Sprintf("(^\\s*%s\\s*=)\\s*(\\S[^#]*)", key)
  21. regKV = regexp.MustCompile(patKV)
  22. regSection = regexp.MustCompile(`^\s*\[\s*(\S.*)\s*\]\s*#?`)
  23. )
  24. handler := func(orgLine string) ([]string, bool) {
  25. var (
  26. opUpd = false
  27. hasUpd = false
  28. isNextSection = false
  29. lines = []string{orgLine}
  30. )
  31. for scanner.Scan() {
  32. line := scanner.Text()
  33. smSection := regSection.FindStringSubmatch(line)
  34. if len(smSection) > 1 && !strings.EqualFold(smSection[1], section) {
  35. isNextSection = true
  36. break
  37. }
  38. switch op {
  39. case -1: // delete the pair of key and value
  40. if regKV.FindString(line) != "" {
  41. hasUpd = true
  42. continue
  43. }
  44. case 0: // update
  45. smKV := regKV.FindStringSubmatch(line)
  46. if len(smKV) > 2 {
  47. opUpd = true
  48. if strings.TrimSpace(smKV[2]) != strings.TrimSpace(value) {
  49. rLine := regKV.ReplaceAllString(line, "$1 "+value+" ")
  50. lines = append(lines, rLine)
  51. hasUpd = true
  52. continue
  53. }
  54. }
  55. case 1: // multiple keys
  56. smKV := regKV.FindStringSubmatch(line)
  57. if len(smKV) > 2 {
  58. if strings.TrimSpace(smKV[2]) == strings.TrimSpace(value) {
  59. opUpd = true
  60. }
  61. }
  62. }
  63. lines = append(lines, line)
  64. }
  65. if !opUpd && op != -1 {
  66. lines = append(lines, kvLine)
  67. for i := len(lines) - 2; i > 0; i-- {
  68. if isNull, _ := regexp.MatchString("^\\s*$", lines[i]); isNull {
  69. lines[i], lines[i+1] = lines[i+1], lines[i]
  70. continue
  71. }
  72. break
  73. }
  74. hasUpd = true
  75. }
  76. if isNextSection {
  77. lines = append(lines, scanner.Text())
  78. }
  79. return lines, hasUpd
  80. }
  81. for scanner.Scan() {
  82. orgLine := scanner.Text()
  83. matched, _ := regexp.MatchString(patSection, orgLine)
  84. if matched {
  85. var updLines []string
  86. updLines, writeBufferToFile = handler(orgLine)
  87. if updLines != nil && writeBufferToFile {
  88. for i := range updLines {
  89. buffer.WriteString(updLines[i] + "\n")
  90. }
  91. }
  92. hasSection = true
  93. continue
  94. }
  95. buffer.WriteString(orgLine + "\n")
  96. }
  97. if !hasSection && op != -1 {
  98. buffer.WriteString("\n[" + section + "]\n")
  99. buffer.WriteString(kvLine + "\n")
  100. writeBufferToFile = true
  101. }
  102. if writeBufferToFile {
  103. if _, err := file.Seek(0, 0); err != nil {
  104. return err
  105. }
  106. if err := file.Truncate(0); err != nil {
  107. return err
  108. }
  109. if _, err := file.Write(buffer.Bytes()); err != nil {
  110. return err
  111. }
  112. }
  113. return nil
  114. }

参考链接: