一个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开发相关内容,望大家感兴趣的支持一下,在此特别感谢。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
`json.Unmarshal` 是 Go 语言标准库中的函数,用于将 JSON 格式的数据解析成 Go 语言的数据结构。它的函数原型如下: ```go func Unmarshal(data []byte, v interface{}) error ``` 其中,`data` 是包含 JSON 数据的字节切片,`v` 是一个指向 Go 语言数据结构的指针。该函数会将 JSON 数据解析成 Go 语言的数据结构,并将结果存储在 `v` 所指向的变量中。 例如,我们可以将以下 JSON 数据解析成 Go 语言的结构体: ```json { "name": "Alice", "age": 18, "score": [98, 99, 100] } ``` 对应的 Go 语言结构体为: ```go type Student struct { Name string Age int Score []int } ``` 我们可以使用 `json.Unmarshal` 函数将 JSON 数据解析成该结构体的实例: ```go data := []byte(`{"name":"Alice","age":18,"score":[98,99,100]}`) var s Student err := json.Unmarshal(data, &s) if err != nil { fmt.Println("json unmarshal error:", err) } fmt.Println(s) ``` 输出将是: ``` {Name:Alice Age:18 Score:[98 99 100]} ``` 注意,`json.Unmarshal` 函数解析 JSON 数据时,会根据 Go 语言数据结构的字段名和 JSON 数据的属性名进行匹配。因此,在上面的示例中,JSON 数据中的 `"name"` 属性名会被解析成 Go 语言结构体中的 `Name` 字段。如果 JSON 数据的属性名和 Go 语言结构体的字段名不一致,可以使用 `json` 标签来进行映射。例如: ```go type Student struct { Name string `json:"student_name"` Age int `json:"student_age"` Score []int `json:"student_score"` } ``` 这样,`json.Unmarshal` 函数在解析 JSON 数据时就会使用 `"student_name"`、`"student_age"` 和 `"student_score"` 属性名分别对应 `Name`、`Age` 和 `Score` 字段。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值