一、sturct json tag的使用
1.tag格式說明
struct json tag主要在struct與json數據轉換的過程(Marshal/Unmarshal)中使用。
json的tag格式如下:
Key type `json:"name,opt1,opt2,opts..."`
說明:
- 變量必須是可導出的(Key首字母必須大寫),否則會被忽略處理。
- 沒有json tag或者tag中name省略(但不能少了
","
),默認使用字段名。 - name要注意命名的有效性。
opt1
、opt2
等項爲可選項,必須使用有限的幾個限定的opt的一個或組合,如"omitempty"
、"string"
,使用非限定的opt會發生錯誤。
2.具體使用格式說明
我們先介紹下源碼文檔中提供的幾種使用方式:
因Marshal與Unmarshal是相反的過程,兩者規則是一致的,以下介紹中僅說明了Marshal時的處理。
(1)不指定tag
Field int // “Filed”:0
不指定tag,默認使用變量名稱。轉換爲json時,key爲Filed。
(2)直接忽略
Field int
json:"-"
//注意:必須爲"-",不能帶有opts
轉換時不處理。
(3)指定key名
Field int
json:"myName"
// “myName”:0
轉換爲json時,key爲myName
(4)"omitempty"零值忽略
Field int
json:",omitempty"
轉換爲json時,值爲零值則忽略,否則key爲myName
(5)指定key且零值忽略
Field int
json:"myName,omitempty"
轉換爲json時,值爲零值則忽略,否則key爲myName
(6)指定key爲"-"
Field int
json:"-,"
// “-”:0
此項與忽略的區別在於多了個”,“。
(7)“string” opt
以上提到的用法都是常見的,這個比較特殊。
"string"僅適用於字符串、浮點、整數或布爾類型,表示的意思是:將字段的值轉換爲字符串;解析時,則是將字符串解析爲指定的類型。主要用於與javascript通信時數據的轉換。
注意:
僅且僅有"string",沒有int、number之類的opt。即帶"string" opt的字段,編碼時僅能將字符串、浮點、整數或布爾類型轉換爲string類型,反之則不然;解碼時可以將string轉換爲其他類型,反之不然。因爲"string"有限制。
Int64String int64
json:",string"
// “Int64String”:“0”
“string” opt的使用可以在Marshal/Unmarshal時自動進行數據類型的轉換,減少了手動數據轉換的麻煩,但是一定要注意使用的範圍,對不滿足的類型使用,是會報錯的。
猜下對string使用"string" opt的結果會是如何呢?
Int64String string
json:",string"
我們在瞭解源碼後解答。
二、源碼角度的設計處理過程
一切的使用方式肯定在設計時就已限定,我們現在看看源碼中的處理過程。
在看實現的過程中,可以思考下使用的方式對不對,還有要注意的地方嗎?
對某些地方非常好的實現思路,我們也可以借鑑下,對以後的編程學習大有裨益。
此處爲了簡潔,具體調用過程略過不講,直接查看核心代碼部分,有興趣的話,可以查看下完整過程。
1.typeFields
在typeFields中詳細的對上面提到的各種用法的tag做了處理,處理後的數據存入fileds,最後在進行編碼。
// typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs.
func typeFields(t reflect.Type) structFields {
// Anonymous fields to explore at the current level and the next.
current := []field{}
next := []field{{typ: t}}
// Count of queued names for current level and the next.
var count, nextCount map[reflect.Type]int
// Types already visited at an earlier level.
visited := map[reflect.Type]bool{}
// Fields found.
var fields []field
// Buffer to run HTMLEscape on field names.
var nameEscBuf bytes.Buffer
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, map[reflect.Type]int{}
for _, f := range current {
if visited[f.typ] {//已處理的過類型跳過
continue
}
visited[f.typ] = true
// Scan f.typ for fields to include.
for i := 0; i < f.typ.NumField(); i++ {
sf := f.typ.Field(i)
isUnexported := sf.PkgPath != ""
if sf.Anonymous {//內嵌類型的處理
t := sf.Type
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if isUnexported && t.Kind() != reflect.Struct {
// Ignore embedded fields of unexported non-struct types.
continue//非struct結構的不能導出的key直接跳過
}
// Do not ignore embedded fields of unexported struct types
// since they may have exported fields.
} else if isUnexported {
// Ignore unexported non-embedded fields.
continue//不能導出的key直接跳過
}
tag := sf.Tag.Get("json")
if tag == "-" {
continue//tag爲"-"直接跳過
}
name, opts := parseTag(tag)
if !isValidTag(name) {
name = ""//包含特殊字符的無效name
}
index := make([]int, len(f.index)+1)
copy(index, f.index)
index[len(f.index)] = i
ft := sf.Type
if ft.Name() == "" && ft.Kind() == reflect.Ptr {
// Follow pointer.
ft = ft.Elem()
}
// Only strings, floats, integers, and booleans can be quoted.
quoted := false
if opts.Contains("string") {//此處爲"string" opt的特殊處理,支持的類型如下:
switch ft.Kind() {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64,
reflect.String:
quoted = true
}
}
// Record found field and index sequence.
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := name != ""
if name == "" {
name = sf.Name//未指定或者指定name無效的使用原field的name
}
field := field{
name: name,
tag: tagged,
index: index,
typ: ft,
omitEmpty: opts.Contains("omitempty"),//omitempty確認
quoted: quoted,//是否支持"string" opt
}
field.nameBytes = []byte(field.name)
field.equalFold = foldFunc(field.nameBytes)
// Build nameEscHTML and nameNonEsc ahead of time.
//兩種格式的構建
nameEscBuf.Reset()
nameEscBuf.WriteString(`"`)
HTMLEscape(&nameEscBuf, field.nameBytes)
nameEscBuf.WriteString(`":`)
field.nameEscHTML = nameEscBuf.String()
field.nameNonEsc = `"` + field.name + `":`
fields = append(fields, field)//存入fields
if count[f.typ] > 1 {
// If there were multiple instances, add a second,
// so that the annihilation code will see a duplicate.
// It only cares about the distinction between 1 or 2,
// so don't bother generating any more copies.
fields = append(fields, fields[len(fields)-1])
}
continue
}
// Record new anonymous struct to explore in next round.
nextCount[ft]++
if nextCount[ft] == 1 {
next = append(next, field{name: ft.Name(), index: index, typ: ft})
}
}
}
}
...
for i := range fields {
f := &fields[i]
f.encoder = typeEncoder(typeByIndex(t, f.index))//設置fields的encoder
}
nameIndex := make(map[string]int, len(fields))
for i, field := range fields {
nameIndex[field.name] = i
}
return structFields{fields, nameIndex}
}
2.encode
func newStructEncoder(t reflect.Type) encoderFunc {
se := structEncoder{fields: cachedTypeFields(t)}
return se.encode
}
func (se structEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
next := byte('{')
FieldLoop:
for i := range se.fields.list {
f := &se.fields.list[i]
// Find the nested struct field by following f.index.
fv := v
for _, i := range f.index {
if fv.Kind() == reflect.Ptr {
if fv.IsNil() {
continue FieldLoop
}
fv = fv.Elem()
}
fv = fv.Field(i)
}
if f.omitEmpty && isEmptyValue(fv) {//"omitempty"的忽略處理,需要值爲零值
continue
}
e.WriteByte(next)
next = ','
if opts.escapeHTML {
e.WriteString(f.nameEscHTML)
} else {
e.WriteString(f.nameNonEsc)
}
opts.quoted = f.quoted
f.encoder(e, fv, opts)//根據具體類型的編碼處理
}
if next == '{' {
e.WriteString("{}")
} else {
e.WriteByte('}')
}
}
以下以int類型intEncoder爲例:
func intEncoder(e *encodeState, v reflect.Value, opts encOpts) {
b := strconv.AppendInt(e.scratch[:0], v.Int(), 10)
if opts.quoted {//帶有"string" opt添加引號
e.WriteByte('"')
}
e.Write(b)
if opts.quoted {
e.WriteByte('"')
}
}
對於數字類型,如果帶有**“string”**則在寫入正式值前後添加引號。
對於字符串類型,如果帶有**“string”**,原string值再編碼時會添加引號,再對結果添加引號,則格式異常,因此需要先對原值進行編碼。
func stringEncoder(e *encodeState, v reflect.Value, opts encOpts) {
if v.Type() == numberType {
numStr := v.String()
// In Go1.5 the empty string encodes to "0", while this is not a valid number literal
// we keep compatibility so check validity after this.
if numStr == "" {
numStr = "0" // Number's zero-val
}
if !isValidNumber(numStr) {
e.error(fmt.Errorf("json: invalid number literal %q", numStr))
}
e.WriteString(numStr)
return
}
if opts.quoted {
sb, err := Marshal(v.String())//注意此處處理
if err != nil {
e.error(err)
}
e.string(string(sb), opts.escapeHTML)
} else {
e.string(v.String(), opts.escapeHTML)
}
}
func (e *encodeState) string(s string, escapeHTML bool) {
e.WriteByte('"')//添加引號
start := 0
for i := 0; i < len(s); {
if b := s[i]; b < utf8.RuneSelf {//字符串中存在特殊的字符時的轉義處理
if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) {
i++
continue
}
if start < i {
e.WriteString(s[start:i])
}
e.WriteByte('\\')
switch b {
case '\\', '"':
e.WriteByte(b)
case '\n':
e.WriteByte('n')
case '\r':
e.WriteByte('r')
case '\t':
e.WriteByte('t')
default:
// This encodes bytes < 0x20 except for \t, \n and \r.
// If escapeHTML is set, it also escapes <, >, and &
// because they can lead to security holes when
// user-controlled strings are rendered into JSON
// and served to some browsers.
e.WriteString(`u00`)
e.WriteByte(hex[b>>4])
e.WriteByte(hex[b&0xF])
}
i++
start = i
continue
}
c, size := utf8.DecodeRuneInString(s[i:])
if c == utf8.RuneError && size == 1 {
if start < i {
e.WriteString(s[start:i])
}
e.WriteString(`\ufffd`)
i += size
start = i
continue
}
// U+2028 is LINE SEPARATOR.
// U+2029 is PARAGRAPH SEPARATOR.
// They are both technically valid characters in JSON strings,
// but don't work in JSONP, which has to be evaluated as JavaScript,
// and can lead to security holes there. It is valid JSON to
// escape them, so we do so unconditionally.
// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
if c == '\u2028' || c == '\u2029' {
if start < i {
e.WriteString(s[start:i])
}
e.WriteString(`\u202`)
e.WriteByte(hex[c&0xF])
i += size
start = i
continue
}
i += size
}
if start < len(s) {
e.WriteString(s[start:])
}
e.WriteByte('"')
}
在瞭解完源碼的處理過程後,我們對之前提到的問題做個解答。對string類型的字段添加"string" opt,得到的是:
Int64String string
json:",string"
// “Int64String”: "“1234"”
三、總結
本文主要從源碼的角度說明struct json tag的爲什麼這麼使用,以及使用時需要注意的地方。最後重複下重要的幾點:
- 字段必須可導出,tag纔有意義
- 忽略必須使用
json:"-"
,不得帶有opts,否則key將會變成"-"
"string"
opt僅適用於字符串、浮點、整數及布爾類型,意思是可以將這些類型的數據Marshal爲string類型,或者將string類型的數據Unmarshal爲這些類型。
請勿濫用,尤其是對已經是string類型的數據使用。