Go中json序列化函数,该传指针或非指针,及其问题思考

346 阅读4分钟

在 Go 语言中,json.Marshaljson.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 是一个指向底层数组的指针。
  • lenslice 的当前长度。
  • capslice 的容量。

当你创建一个 slice 时,实际上是在栈上创建了一个包含 ptrlencap 的结构体。ptr 指向的底层数组存储在堆上。

当你传递 slice 给一个函数时

  • 直接传递 slice:传递的是这个结构体的副本。因此,函数接收的 slice 仍然拥有 ptrlencap,但它们只是原始 slice 的副本。对 ptr 指向的底层数组进行的任何修改都会反映到原始 slice 上, len cap 的修改不会影响原始 slice
  • 传递 slice 的指针:传递的是这个 slice 结构体本身的指针。这样,函数可以修改 slice 的所有三个字段,包括 ptrlencap,这些修改会直接影响到原始 slice

为什么需要传指针

  1. 改变 slice 的长度和容量: json.Unmarshal 在解码 JSON 数据时,可能需要调整 slice 的长度和容量。要做到这一点,需要修改 slice 结构体中的 lencap 字段。如果直接传递 slice,而不是它的指针,Unmarshal 操作无法改变 slice 的长度和容量,只能修改它引用的底层数组的数据,而不能扩展这个数组。
  2. 创建新的底层数组: 如果 JSON 数据的长度超过了 slice 的当前容量,Unmarshal 可能需要分配一个新的底层数组,并更新 sliceptr 字段。为了做到这一点,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 可以修改 slicelencapptr,确保解码后的数据能正确存储。

所以,即使 slice 是引用类型,在需要改变其长度、容量或指向的底层数组时,仍然需要使用指针。