Golang中interface{}和指针相关的怪异现象解析

Golang中interface{}和指针相关的怪异现象解析 我正在编写一个库,用于调用某些返回JSON响应的API。显然,我的库需要将这个JSON响应解析成更友好的格式,比如结构体。但是,我希望让用户能够灵活定义自己的结构体(例如,为了适应不同的命名方案)。为此,用户需要提供一个模板结构体,用于解析JSON响应。由于这个用户提供的结构体类型未知,我们必须使用interface{}

以下是我的尝试:

package library

import (
    "encoding/json"
)

// 假设API始终返回此数据
var mockResponse = []byte(`{ "id": 123 }`)
var Template interface{}

func FetchData() interface{} {
    // 执行数据获取操作
    // 假设JSON反序列化始终成功
    _ = json.Unmarshal(mockResponse, &Template)
    return Template
}

理想情况下,我希望用户这样使用这个库:

package main

import (
    "library"
)

type MyStruct struct {
    ID int `json:"id"`
}

func main() {
    library.Template = MyStruct{}
    val := library.FetchData()
    // 然后使用类型断言将`val`转换为MyStruct
}

有趣的是,如果这样做,val会变成map[string][]interface{},这不是我们想要的结果。经过一些尝试,如果我改用library.Template = &MyStruct{}val就会变成*MyStruct。这样更接近目标。

这里有一个重现示例:https://repl.it/@yihangho/Weird-interface-and-pointers

应该如何理解这种奇怪的现象?


更多关于Golang中interface{}和指针相关的怪异现象解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

这是个合理的建议。谢谢!但我仍然很好奇为什么我发布的代码会有那样的行为。

更多关于Golang中interface{}和指针相关的怪异现象解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你可能希望将代码结构设计得更简单,例如参考 https://play.golang.org/p/qec5R1fkkJO

在Go包的文档中,关于json.Unmarshal部分我们发现 😊
要将JSON反序列化为接口值,Unmarshal会在接口值中存储以下类型之一:

  • bool,对应JSON布尔值
  • float64,对应JSON数值
  • string,对应JSON字符串
  • []interface{},对应JSON数组
  • map[string]interface{},对应JSON对象
  • nil,对应JSON空值

我想对 yihangho 补充说明一下本讨论串中其他人已经提到的观点。通过编写一个简单的 Unmarshal 函数并尝试处理传入的不同值,能帮助我理解您尝试的操作为何不成功:

func main() {
    // 示例代码
}

点击查看代码示例

虽然 @clbanning 已经解释了传入值类型的区别,但我认为从解组操作的"另一侧"来观察可能会有所启发。

此外,根据 https://golang.org/pkg/encoding/json/#Unmarshal:

要将JSON解组到指针中,Unmarshal首先处理JSON为JSON字面量null的情况。
在这种情况下,Unmarshal将指针设置为nil。否则,Unmarshal将JSON解组到指针所指向的值。
如果指针为nil,Unmarshal会为其分配一个新值以供指向。

如果您查看 go/src/encoding/json/decode.go 中的代码(第428-432行),会发现遍历 ‘v’ 时遇到的第一个非指针被用作值的类型。

“Template = MyStruct{}” 是接口类型,而 &Template 是指向 interface{} 的指针,其本身并未解析为指针;因此 JSON 对象被解组为 map[string]interface{} 值。

“Template = &MyStruct{}” 是 interface{} 类型,但确实解析为指针,因此会"向下遍历"到 MyStruct{},这就是 ‘v’ 的类型,所以 JSON 对象被解组为 MyStruct{}。

在Go语言中,interface{}与指针结合使用时确实会出现一些看似怪异的行为,这主要与Go的类型系统和json.Unmarshal的工作机制有关。

问题分析

当您使用library.Template = MyStruct{}时,实际上发生的是:

  1. Template接口变量存储了MyStruct的值副本
  2. json.Unmarshal(&Template, mockResponse)尝试反序列化到这个接口变量
  3. 由于接口变量当前包含具体类型信息,json.Unmarshal会创建一个新的map[string]interface{}来存储解析结果

而当您使用library.Template = &MyStruct{}时:

  1. Template接口变量存储了*MyStruct指针
  2. json.Unmarshal可以直接修改指针指向的结构体内容

解决方案

正确的做法是让用户始终传递指针到接口中:

package library

import (
    "encoding/json"
)

var mockResponse = []byte(`{ "id": 123 }`)
var Template interface{}

func FetchData() interface{} {
    if Template == nil {
        panic("Template must be set before calling FetchData")
    }
    
    // 必须传递Template的地址给json.Unmarshal
    err := json.Unmarshal(mockResponse, Template)
    if err != nil {
        panic(err)
    }
    return Template
}

用户使用方式:

package main

import (
    "fmt"
    "library"
)

type MyStruct struct {
    ID int `json:"id"`
}

func main() {
    // 正确:传递指针到接口中
    library.Template = &MyStruct{}
    val := library.FetchData()
    
    // 类型断言获取具体类型
    if myStruct, ok := val.(*MyStruct); ok {
        fmt.Printf("ID: %d\n", myStruct.ID) // 输出: ID: 123
    }
}

更健壮的实现

为了避免用户错误使用,可以提供一个类型安全的包装函数:

package library

import (
    "encoding/json"
)

var mockResponse = []byte(`{ "id": 123 }`)

func FetchData(template interface{}) error {
    // 检查template是否为指针
    if template == nil {
        return json.Unmarshal(mockResponse, template)
    }
    
    // 使用反射检查是否为指针类型
    v := reflect.ValueOf(template)
    if v.Kind() != reflect.Ptr {
        return fmt.Errorf("template must be a pointer, got %T", template)
    }
    
    return json.Unmarshal(mockResponse, template)
}

使用方式:

func main() {
    var myStruct MyStruct
    err := library.FetchData(&myStruct)
    if err != nil {
        panic(err)
    }
    fmt.Printf("ID: %d\n", myStruct.ID) // 输出: ID: 123
}

关键理解点:json.Unmarshal需要能够修改目标变量的能力,因此必须接收指针。当通过接口传递时,接口本身已经包含了类型信息,但json.Unmarshal仍然需要能够修改底层数据。

回到顶部