使用 Go 的 struct tag 来解析版本号字符串

作者: caixw
修改时间:

各类软件的版本号定义虽然都不尽相同,但是其基本原理基本上还是相通的:通过特写的字符对字符串进行分割。我们把这一规则稍作整理,放到 struct tag 中,告诉解析器如何解析,下面就以 semver 为例作个示范:

1type SemVersion struct {
2    Major      int    `version:"0,.1"`
3    Minor      int    `version:"1,.2"`
4    Patch      int    `version:"2,+4,-3"`
5    PreRelease string `version:"3,+4"`
6    Build      string `version:"4"`
7}

按以下方式根据 struct tag 标注的顺序执行:

  • 其中 struct tag 中的第一段内容表示的是当前字段的一个编号,要求唯一且为数值,0 表示入口;
  • 后面的是解析规则,可以有多条,以逗号分隔,优先级等同;
  • 每一条规则的第一个字符为触发条件,之后的数字即为前面的编号,当解析器碰到该字符时,即结束当前字段的解析,跳转到其后面指定的编号字段。

如何实现

首先定义一个表示每个字段的结构:

1type struct field {
2    value  reflect.Value // 指赂字段的值
3    routes map[byte]int  // 解析的跳转规则
4}

然后将整个结构体解析到一个 map 中,其键名即为字段的编号:

 1func getFields(obj interface{}) (map[int]*field, error) {
 2    v := reflect.ValueOf(obj)
 3    t := v.Type()
 4    fields := make(map[int]*field, v.NumField())
 5
 6    for i := 0; i < v.NumField(); i++ {
 7        tags := strings.Split(t.Field(i).Tag.Get("version"), ",")
 8        if len(tags) < 1 {
 9            return nil, errors.New("缺少标签内容")
10        }
11
12        index, err := strconv.Atoi(tags[0])
13        if err != nil {
14            return nil, err
15        }
16        if _, found := fields[index]; found {
17            return nil, errors.New("重复的字段编号")
18        }
19
20        field := &field{routes: make(map[byte]int, 2)}
21
22        for _, vv := range tags[1:] {
23            n, err := strconv.Atoi(vv[1:])
24            if err != nil {
25                return nil, err
26            }
27            field.routes[vv[0]] = n
28        }
29
30        field.value = v.Field(i)
31        fields[index] = field
32    }
33
34    return fields, nil
35}

然后通过一个函数将字符串解析到结构中:

 1func Parse(obj interface{}, ver string) {
 2    fields, _ := getFields(obj)
 3
 4    start := 0
 5    field := fields[0]
 6    for i := 0; i < len(ver)+1; i++ {
 7        var nextIndex int
 8        if i < len(ver) { // 未结束
 9            index, found := field.routes[ver[i]]
10            if !found {
11                continue
12            }
13            nextIndex = index
14        }
15
16        switch field.value.Kind() {
17        case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
18             reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
19            n, err := strconv.ParseInt(ver[start:i], 10, 64)
20            if err != nil {
21                panic(err)
22            }
23            field.value.SetInt(n)
24        case reflect.String:
25            field.value.SetString(ver[start:i])
26        default:
27            panic("无效的类型")
28        }
29
30        i++ // 过滤掉当前字符
31        start = i
32        field = fields[nextIndex] // 下一个 field
33    } // end for
34}

之后我们只需要定义各类版本号的相关结构体,然后传给 Parse 就可以了:

 1// Major_Version_Number.Minor_Version_Number[.Revision_Number[.Build_Number]]
 2type GNUVersion struct {
 3    Major    int    `version:"0,.1"`
 4    Minor    int    `version:"1,.2"`
 5    Revision int    `version:"2, 3"`
 6    Build    string `version:"3"`
 7}
 8
 9gnu := &GNUVersion{}
10sem := &SemVersion{}
11Parse(gnu, "1.2.0 build-1124")
12Parse(sem, "1.2.0+20160615")

查看完整的实现:https://github.com/issue9/version