原文链接:Dave Cheney的博文
先从一段代码说起:
package main
import (
"encoding/json"
"fmt"
)
type Result struct {
Foo string
}
func main() {
content := `{"foo": "bar"}`
res := &Result{}
err := json.Unmarshal([]byte(content), &res)
if err != nil {
panic(err)
}
fmt.Printf("res = %+v\n", res) // 正常返回
res2 := &Result{}
err = json.Unmarshal([]byte(content), res2)
if err != nil {
panic(err)
}
fmt.Printf("res2 = %+v\n", res2) // 正常返回
var res3 *Result
err = json.Unmarshal([]byte(content), &res3)
if err != nil {
panic(err)
}
fmt.Printf("res3 = %+v\n", res3) // 正常返回
var res4 *Result
err = json.Unmarshal([]byte(content), res4)
if err != nil {
panic(err)
}
fmt.Printf("res4 = %+v\n", res4) // panic!!!
}
这是一段并不复杂的代码:尝试将一段文本反序列化到一个go 结构体,示例中给出四种定义,其中只有一种情况发生了报错,我们接下来就从这个异常情况来简单说下go
语言体系中,空指针的概念。
在上面代码示例中,发生报错的res4
和res2
从数据类型上来说并无不同,都是一个指向Result
结构体的指针。我们来简单看下发生报错时我们拿到的提示json: Unmarshal(nil *main.Result)
。看上去结果比较清晰,json.Unmarshal
并不接受空指针(nil pointer)。
事实是这样吗?我们去encoding/json
的文档中查看一下:
Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v. If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError.(https://pkg.go.dev/encoding/json?tab=doc#Unmarshal
)
果然,文档完善的标准库给了我们足够清晰的结论:对于非指针和空指针的参数,会抛出InvalidUnmarshalError
。
那么,encoding/json
的接口又是为什么这么设计呢?这里我们要先清楚以下两个前提:
-
go
中的赋值操作都是传值的,这里的赋值包括变量的定义初始化,变量的绑定,函数参数传递。 -
go
的空指针和指向空值的指针有本质上区别:指向空值的指针本身指向的是一个空的对象(取决于指针的类型),被指向的对象本身是一个已经分配好的空间;空指针就是指针的空值nil
,本身不指向任一对象。
基于以上两个前提,我们再来看下Unmarshal
的函数定义:
func Unmarshal(data []byte, v interface{}) error
函数本身的返回值并不包含反序列之后的对象,只有一个描述结果的error
返回值。所以Unmarshal
的函数行为一定是通过改写v
对象来达到的。根据第一条前提,我们知道如果v
本身是非指针的话,Unmarshal
的改写行为无法影响传递进来的值对象的原始值;又根据第二条前提,v
如果是nil指针的话,函数无法根据指针去改写它指向的对象。
一个Unmarshal
的文档描述,引出了go语言的两个知识点,还是挺有趣的。