天天看點

一個json Unmarshal to slice的問題及延伸

前言

Go語言愛好者周刊:第 65 期的刊首語貼出這麼一道題:

package main

import (
   "encoding/json"
   "fmt"
)

type AutoGenerated struct {
   Age   int    `json:"age"`
   Name  string `json:"name"`
   Child []int  `json:"child"`
}

func main() {
   jsonStr1 := `{"age": 14,"name": "potter", "child":[1,2,3]}`
   a := AutoGenerated{}
   json.Unmarshal([]byte(jsonStr1), &a)
   aa := a.Child
   fmt.Println(aa)
   jsonStr2 := `{"age": 12,"name": "potter", "child":[3,4,5,7,8,9]}`
   json.Unmarshal([]byte(jsonStr2), &a)
   fmt.Println(aa)
}
           

他看到了網上的解釋,想不通。我看了下,他看到的解釋是不對的。

你覺得輸出是什麼呢?

A:[1 2 3] [1 2 3] ;B:[1 2 3] [3 4 5]; C:[1 2 3] [3 4 5 6 7 8 9];D:[1 2 3] [3 4 5 0 0 0]

問題解答

題目主要考究的就是slice的特性。

slice的三要素,指針(指向底層數組)、長度和容量。

代碼中可以看出,slice aa變量除列印外,并沒有參與其他的處理,根據slice的三要素,我們可知aa的長度和容量不會發生變化,是以可以排除C、D選項。

其次,aa持有的是a.Child的底層數組,其長度與cap對應a.Child的長度及cap。第一次json.Unmarshal,a.Child的結果應為

[]int{1,2,3}

,長度為3,則

aa=a.Child[:3]

。再次Unmarshal後a.Child對應的slice為

[]int{3,4,5,7,8,9}

,則aa對應的就是

a.Child[:3]

,則aa的結果為[]int{3,4,5}。

問題延伸

前後兩次解析後,a.Child的len和cap如何變化的?

func main() {
    jsonStr1 := `{"age": 14,"name": "potter", "child":[1,2,3]}`
    a := AutoGenerated{}
    json.Unmarshal([]byte(jsonStr1), &a)
    fmt.Println(len(a.Child),cap(a.Child))
    jsonStr2 := `{"age": 12,"name": "potter", "child":[3,4,5,7,8,9]}`
    json.Unmarshal([]byte(jsonStr2), &a)
    fmt.Println(len(a.Child),cap(a.Child))
}
           

關于len的話,我們可以明确地指導,其長度與解析資料的長度一緻。那麼cap呢?

我們直接把代碼運作下,得到先後兩次的cap為4,6。為什麼會是這樣呢?

第一次運作後的a.Child是不足以容納第二次的資料的,在第二次解析時肯定需要擴容?我們了解的append的擴容規則,在小于1024時,通常是翻倍擴容,這裡的結果卻不是?說明其采用的并不是append的擴容。那麼json.Unmarshal中slice又是怎麼擴容的呢?

問題追蹤

解析json數組的核心代碼如下:

// array consumes an array from d.data[d.off-1:], decoding into v.
// The first byte of the array ('[') has been read already.
func (d *decodeState) array(v reflect.Value) error {
	// Check for unmarshaler.
	u, ut, pv := indirect(v, false)
	if u != nil {
		start := d.readIndex()
		d.skip()
		return u.UnmarshalJSON(d.data[start:d.off])
	}
	if ut != nil {
		d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)})
		d.skip()
		return nil
	}
	v = pv

	// Check type of target.
	switch v.Kind() {
	case reflect.Interface:
		if v.NumMethod() == 0 {
			// Decoding into nil interface? Switch to non-reflect code.
			ai := d.arrayInterface()
			v.Set(reflect.ValueOf(ai))
			return nil
		}
		// Otherwise it's invalid.
		fallthrough
	default:
		d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)})
		d.skip()
		return nil
	case reflect.Array, reflect.Slice:
		break
	}

	i := 0
	for {
		// Look ahead for ] - can only happen on first iteration.
		d.scanWhile(scanSkipSpace)
		if d.opcode == scanEndArray {
			break
		}

		// Get element of array, growing if necessary.
		if v.Kind() == reflect.Slice {
			// Grow slice if necessary
			if i >= v.Cap() {
				newcap := v.Cap() + v.Cap()/2
				if newcap < 4 {
					newcap = 4
				}
				newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
				reflect.Copy(newv, v)
				v.Set(newv)
			}
			if i >= v.Len() {
				v.SetLen(i + 1)
			}
		}

		if i < v.Len() {
			// Decode into element.
			if err := d.value(v.Index(i)); err != nil {
				return err
			}
		} else {
			// Ran out of fixed array: skip.
			if err := d.value(reflect.Value{}); err != nil {
				return err
			}
		}
		i++

		// Next token must be , or ].
		if d.opcode == scanSkipSpace {
			d.scanWhile(scanSkipSpace)
		}
		if d.opcode == scanEndArray {
			break
		}
		if d.opcode != scanArrayValue {
			panic(phasePanicMsg)
		}
	}

	if i < v.Len() {
		if v.Kind() == reflect.Array {
			// Array. Zero the rest.
			z := reflect.Zero(v.Type().Elem())
			for ; i < v.Len(); i++ {
				v.Index(i).Set(z)
			}
		} else {
			v.SetLen(i)
		}
	}
	if i == 0 && v.Kind() == reflect.Slice {
		v.Set(reflect.MakeSlice(v.Type(), 0, 0))
	}
	return nil
}
           

其中關于擴容的核心邏輯如下:

// Get element of array, growing if necessary.
		if v.Kind() == reflect.Slice {
			// Grow slice if necessary
			if i >= v.Cap() {
				newcap := v.Cap() + v.Cap()/2
				if newcap < 4 {
					newcap = 4
				}
				newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
				reflect.Copy(newv, v)
				v.Set(newv)
			}
			if i >= v.Len() {
				v.SetLen(i + 1)
			}
		}
           

當slice需要擴容時,則增長一半,newcap變為

cap+cap/2

(這是第二次cap為6的原因);若

newcap<4

,則置

newcap=4

(這是第一次cap為4的原因)。

擴容的slice是主動調用reflect.MakeSlice生成的。

延伸一下,第三次解析代碼如下,則a.Child的cap将會是多少呢?

jsonStr3 := `{"age": 12,"name": "potter", "child":[7,8,9,0,1,2,3,4,5,6]}`
    json.Unmarshal([]byte(jsonStr2), &a)
           

總結

本文主要從一個問題開始,詳細解析了關于slice及json.Unmarshal到slice的部分特性。

slice

  • slice的三要素,指針(指向底層數組)、長度和容量。
  • 未參與運算(擴容、進一步slice)的話,長度和容量是不會變化。
  • 由于其持有的是底層數組的指針,是以數組資料發生變化,其資料也會發生變化

json unmarshal to slice

  • 具體擴容規則如下:
newcap := v.Cap() + v.Cap()/2
if newcap < 4 {
		newcap = 4
	}
           
  • 使用建議:

在使用json Umarshal時,如果知道slice的長度,則提前初始化長度,則可以減少擴容的過程,提高資料解析的速度。

公衆号

鄙人剛剛開通了公衆号,專注于分享Go開發相關内容,望大家感興趣的支援一下,在此特别感謝。

一個json Unmarshal to slice的問題及延伸