在 Go 语言中,json.Marshal
和 json.Unmarshal
是用于处理 JSON 数据的两个关键函数。
它们的使用涉及到传递指针或非指针类型的细微差别。
json.Marshal
json.Marshal
函数将 Go 语言的值序列化为 JSON 字符串。在使用 json.Marshal
时,通常传递非指针类型即可。
- 传值类型: 当你传递一个结构体、切片、映射、数组等类型时,
json.Marshal
会自动将这些值序列化为 JSON。 示例:
type Person struct {
Name string
Age int
}
person := Person{Name: "Alice", Age: 30}
jsonData, err := json.Marshal(person) // 传递值类型
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonData)) // 输出: {"Name":"Alice","Age":30}
- 传指针类型: 虽然可以传递指针类型给
json.Marshal
,但这不是必需的,因为 Go 会自动处理指针的解引用。也就是说,无论是值还是指针,结果都是相同的。 示例:
personPtr := &Person{Name: "Alice", Age: 30}
jsonData, err := json.Marshal(personPtr) // 传递指针类型
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonData)) // 输出: {"Name":"Alice","Age":30}
json.Unmarshal
json.Unmarshal
函数用于将 JSON 字符串解析为 Go 语言的值。在使用 json.Unmarshal
时,必须传递指针类型,以便函数能够修改目标对象。
- 传指针类型: 因为
json.Unmarshal
需要将解析后的数据写入目标变量中,所以需要传递目标变量的指针。只有传递指针,json.Unmarshal
才能修改目标变量的值。 示例:
jsonData := `{"Name":"Alice","Age":30}`
var person Person
err := json.Unmarshal([]byte(jsonData), &person) // 传递指针类型
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", person) // 输出: {Name:Alice Age:30}
- 不传指针会报错: 如果你不传递指针,而是传递一个值,
json.Unmarshal
将无法修改目标值,并且会报错。 示例:
var person Person
err := json.Unmarshal([]byte(jsonData), person) // 不传递指针会报错
if err != nil {
log.Fatal(err) // 会在这里报错:json: Unmarshal(non-pointer main.Person)
}
总结
json.Marshal
: 既可以传递值也可以传递指针,一般情况下传递值即可。json.Unmarshal
: 必须传递指针,以便函数能够修改目标变量。
问题 1:slice本身就是引用类型,进行反序列化的时候,是不是可以不用传指针呢?
在 Go 中,虽然 slice
是引用类型,但在反序列化时,仍然需要传递指针给 json.Unmarshal
。
Slice 的底层结构
首先,让我们简单回顾一下 Go 中 slice
的底层结构:
type slice struct {
ptr *ElementType // 指向底层数组的指针
len int // 长度
cap int // 容量
}
ptr
是一个指向底层数组的指针。len
是slice
的当前长度。cap
是slice
的容量。
当你创建一个 slice
时,实际上是在栈上创建了一个包含 ptr
、len
和 cap
的结构体。ptr
指向的底层数组存储在堆上。
当你传递 slice
给一个函数时
- 直接传递
slice
:传递的是这个结构体的副本。因此,函数接收的slice
仍然拥有ptr
、len
和cap
,但它们只是原始slice
的副本。对ptr
指向的底层数组进行的任何修改都会反映到原始slice
上,但len
和cap
的修改不会影响原始slice
。 - 传递
slice
的指针:传递的是这个slice
结构体本身的指针。这样,函数可以修改slice
的所有三个字段,包括ptr
、len
和cap
,这些修改会直接影响到原始slice
。
为什么需要传指针
- 改变 slice 的长度和容量:
json.Unmarshal
在解码 JSON 数据时,可能需要调整slice
的长度和容量。要做到这一点,需要修改slice
结构体中的len
和cap
字段。如果直接传递slice
,而不是它的指针,Unmarshal
操作无法改变slice
的长度和容量,只能修改它引用的底层数组的数据,而不能扩展这个数组。 - 创建新的底层数组: 如果 JSON 数据的长度超过了
slice
的当前容量,Unmarshal
可能需要分配一个新的底层数组,并更新slice
的ptr
字段。为了做到这一点,Unmarshal
需要一个指向slice
结构体的指针,以便直接修改ptr
字段。
上述两个要求,对于拷贝 slice 来说,都是做不到的。
反序列化示例代码
package main
import (
"encoding/json"
"fmt"
)
func main() {
// 示例 JSON 数据
jsonData := `[1, 2, 3, 4, 5]`
// 如果你不传递指针,会发生什么?
var data []int
err := json.Unmarshal([]byte(jsonData), data) // 传递 data(非指针)
if err != nil {
fmt.Println("Error:", err) // 会报错:cannot unmarshal array into Go value of type []int
}
// 正确的用法,传递指针
err = json.Unmarshal([]byte(jsonData), &data) // 传递 &data(指针)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Data:", data) // 正常解码出 [1, 2, 3, 4, 5]
}
}
关键点总结
slice
是引用类型,但它的引用性体现在对底层数组的引用上,而不是slice
结构体本身。- 传递指针是为了让
json.Unmarshal
可以修改slice
的len
、cap
和ptr
,确保解码后的数据能正确存储。
所以,即使 slice
是引用类型,在需要改变其长度、容量或指向的底层数组时,仍然需要使用指针。