66

golang如何使用struct的tag属性

 5 years ago
source link: https://studygolang.com/articles/16507?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

golang如何使用struct的tag属性

从一个例子说起

我们经常会碰到下面格式的struct定义:

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

这个struct定义一个叫做Person的类型,包含两个域Name和Age;但是在域的后面有神奇的 json:"name" ,这个用来干什么用?这篇文章试图来解释这个问题。

当golang的对象需要和json做转换的时候,我们就经常用到这个特性。

有两点注意的地方:

  1. 如果一个域不是以大写字母开头的,那么转换成json的时候,这个域是被忽略的。
$ cat main.go
package main

import (
    "fmt"
    "encoding/json"
)

type Person struct {
    Name string `json:"name"`
    age  int    `json:"age"`
}

func main() {
    person := Person { "tom", 12 }
    if b, err := json.Marshal(person); err != nil {
        fmt.Printf("error: %s", err.Error())
    } else {
        fmt.Printf("value: %s", b)
    }
}
$ go build -o main main.go 
$ ./main
value: {"name":"tom"}

我们看到转换成json串之后,name正常输出了,而age被丢弃了,因为age以小写字母开头。

  1. 如果没有使用 json:"name" tag,那么输出的json字段名和域名是一样的。
$ cat main.go
package main

import (
    "fmt"
    "encoding/json"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    person := Person { "tom", 12 }
    if b, err := json.Marshal(person); err != nil {
        fmt.Printf("error: %s", err.Error())
    } else {
        fmt.Printf("value: %s", b)
    }
}
$ go build -o main main.go 
$ ./main
value: {"Name":"tom","Age":12}

我们看到输出的json串使用的是struct定义的字段名。

总结一下, json:"name" 格式串是用来指导json.Marshal/Unmarshal,在进行json串和golang对象之间转换的时候映射字段名使用的。再举一个例子,json串和golang域名字可以任意转换:

$ cat main.go

package main

import (
    "fmt"
    "encoding/json"
)

type Person struct {
    Name string    `json:"age"`
    Age  int       `json:"address"`
}

func main() {
    person := Person { "tom", 12 }
    if b, err := json.Marshal(person); err != nil {
        fmt.Printf("error: %s", err.Error())
    } else {
        fmt.Printf("value: %s", b)
    }
}
$ go build -o main main.go 
$ ./main
value: {"age":"tom","address":12}

这个例子我们把Name映射成了 age,而把Age映射成address,当然这是个奇葩的映射,没有任何正向意义,只有负向意义,只是为了说明可以进行任何名字映射而已。

如果我们去看json包的源代码,我可以看到在encoding/json/encode.go, encoding/json/decode.go里面有读取tag值得相关代码。

tag := sf.Tag.Get("json")

也就是说这个json的tag是被json.Marshal和json.Unmarshal来使用的。

我们如何使用tag

还是以前的例子,Person有一个域Age,我们能不能限定Age的值在1-100之间,不至于太大,否则这个值没有意义了。

$ cat main.go
package main

import (
    "fmt"
    "strings"
    "strconv"
    "reflect"
  _ "encoding/json"
)

type Person struct {
    Name string    `json:"name"`
    Age  int       `json:"age" valid:"1-100"`
}

func (p * Person) validation() bool {
    v := reflect.ValueOf(*p)
    tag := v.Type().Field(1).Tag.Get("valid")
    val := v.Field(1).Interface().(int)
    fmt.Printf("tag=%v, val=%v\n", tag, val)
    
    result := strings.Split(tag, "-")
    var min, max int
    min, _ = strconv.Atoi(result[0])
    max, _ = strconv.Atoi(result[1])

    if val >= min && val <= max {
        return true
    } else {
        return false
    }
}

func main() {
    person1 := Person { "tom", 12 }
    if person1.validation() {
        fmt.Printf("person 1: valid\n")
    } else {
        fmt.Printf("person 1: invalid\n")
    }
    person2 := Person { "tom", 250 }
    if person2.validation() {
        fmt.Printf("person 2 valid\n")
    } else {
        fmt.Printf("person 2 invalid\n")
    }
}

这么例子我们给Person添加了一个validate函数,validate验证age是不是合理。

这个函数可以扩展对任意struct的任意valid域进行验证。

$ cat main.go
package main

import (
    "fmt"
    "strings"
    "strconv"
    "reflect"
  _ "encoding/json"
)

type Person struct {
    Name string    `json:"name"`
    Age  int       `json:"age" valid:"1-100"`
}

type OtherStruct struct {
    Age  int       `valid:"20-300"`
}

func validateStruct(s interface{}) bool {
  v := reflect.ValueOf(s)

  for i := 0; i < v.NumField(); i++ {
    fieldTag    := v.Type().Field(i).Tag.Get("valid")
    fieldName   := v.Type().Field(i).Name
    fieldType   := v.Field(i).Type()
    fieldValue  := v.Field(i).Interface()

    if fieldTag == "" || fieldTag == "-" {
        continue
    }

    if fieldName == "Age" && fieldType.String() == "int" {
        val := fieldValue.(int)

        tmp := strings.Split(fieldTag, "-")
        var min, max int
        min, _ = strconv.Atoi(tmp[0])
        max, _ = strconv.Atoi(tmp[1])
        if val >= min && val <= max {
            return true
        } else {
            return false
        }
    }
  }
  return true
}

func main() {
    person1 := Person { "tom", 12 }
    if validateStruct(person1) {
        fmt.Printf("person 1: valid\n")
    } else {
        fmt.Printf("person 1: invalid\n")
    }

    person2 := Person { "jerry", 250 }
    if validateStruct(person2) {
        fmt.Printf("person 2: valid\n")
    } else {
        fmt.Printf("person 2: invalid\n")
    }

    other1 := OtherStruct { 12 }
    if validateStruct(other1) {
        fmt.Printf("other 1: valid\n")
    } else {
        fmt.Printf("other 1: invalid\n")
    }

    other2 := OtherStruct { 250 }
    if validateStruct(other2) {
        fmt.Printf("other 2: valid\n")
    } else {
        fmt.Printf("other 2: invalid\n")
    }
}

在这个例子中我们定义了一个函数validateStruct,接受任意一个struct作为参数;validateStruct为验证struct中所有定义的Age字段,如果字段名字是Age,字段类型是int,并且定义了valid tag,那么就会验证这个valid是否有效。

看执行结果:

$ go build -o main main.go 
$ ./main
person 1: valid
person 2: invalid
other 1: invalid
other 2: valid

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK