Golang接口签名与内部库接口冲突导致栈溢出问题

Golang接口签名与内部库接口冲突导致栈溢出问题 以下示例代码会导致栈溢出,因为其接口签名与 encoding/json/Marshaler 相同。

json.Marshal(s) 会在内部调用 MarshalJSON,因此最终会形成递归函数调用。

这是设计使然还是设计失误?

package main

import (
	"encoding/json"
	"fmt"
)

type IJSON interface {
	MarshalJSON() ([]byte, error)  // 此接口签名与 "encoding/json/Marshaler" 相同
}

type Student struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func (s *Student) MarshalJSON() ([]byte, error) {
	return json.Marshal(s)
}

func main() {
	s := &Student{"James", 12}

	data, err := s.MarshalJSON()
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s", data)
}

更多关于Golang接口签名与内部库接口冲突导致栈溢出问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

这只是一个示例,说明有人可能会意外地与第三方/标准库接口发生冲突。只是想知道是否有办法避免这种情况。

更多关于Golang接口签名与内部库接口冲突导致栈溢出问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个非常特殊的例子,之所以会出现这种情况是因为编码器使用了反射机制。这种情况并不具有普遍性。

请注意 encoding/json 包并不属于 stdlib

在 Go 语言中,我们使用包来防止命名冲突。

请记住 Go 语言的接口是隐式实现的。既不需要显式声明,也没有 “implements” 关键字。

MarshalJSON 用于希望覆盖默认行为的类型。这个方法工作得很好:

package main

import (
	"encoding/json"
	"fmt"
)

type Student struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	if data, err := json.Marshal(Student{"James", 12}); err != nil {
		panic(err)
	} else {
		fmt.Printf("%s\n", data)
	}
}

你好 Charles,

创建"IJSON"是一个设计错误 🙂

你的接口与"encoding/json/Marshaler"的公共方法冲突 在内部,encode/json 使用反射来确定使用哪个编码器。

通过声明一个方法 func (s *Student) MarshalJSON() ([]byte, error),你的 Student 就符合了 json.Marshaler 接口。

因此编码器会使用你的 MarshalJSON() 方法,结果变成了无限递归调用。

Go 的接口系统是宽松的。

此致,

这是一个典型的递归调用导致的栈溢出问题,这是设计使然而非设计失误。encoding/json包在设计时明确规定了Marshaler接口的行为模式。

问题出现在Student.MarshalJSON方法的实现中:当调用json.Marshal(s)时,JSON包检测到该类型实现了Marshaler接口,于是再次调用MarshalJSON方法,从而形成无限递归。

正确的实现应该避免这种递归调用。以下是几种解决方案:

方案1:使用中间类型避免递归

func (s *Student) MarshalJSON() ([]byte, error) {
	type Alias Student
	return json.Marshal(&struct {
		*Alias
	}{
		Alias: (*Alias)(s),
	})
}

方案2:手动构建JSON

func (s *Student) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf(`{"name":"%s","age":%d}`, s.Name, s.Age)), nil
}

方案3:使用map构建

func (s *Student) MarshalJSON() ([]byte, error) {
	return json.Marshal(map[string]interface{}{
		"name": s.Name,
		"age":  s.Age,
	})
}

第一种方案是最常用的,它通过类型别名避免了递归调用,同时保持了原有的JSON标签和结构。这种设计模式在需要自定义JSON序列化逻辑时非常有用,比如添加额外字段或修改字段格式:

func (s *Student) MarshalJSON() ([]byte, error) {
	type Alias Student
	return json.Marshal(&struct {
		*Alias
		DisplayName string `json:"display_name"`
	}{
		Alias:       (*Alias)(s),
		DisplayName: fmt.Sprintf("%s (%d)", s.Name, s.Age),
	})
}

这种接口设计确保了类型可以完全控制自己的序列化过程,但需要开发者正确实现以避免递归问题。

回到顶部