使用 Go 的 struct tag 来解析版本号字符串
各类软件的版本号定义虽然都不尽相同,但是其基本原理基本上还是相通的:通过特写的字符对字符串进行分割。我们把这一规则稍作整理,放到 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")