在不引起堆栈溢出的情况下,在UnmarshalJSON函数内部调用json.Unmarshal。

huangapple go评论88阅读模式
英文:

Call json.Unmarshal inside UnmarshalJSON function without causing stack overflow

问题

我想在我的UnmarshalJSON实现中执行一些额外的步骤来初始化数据结构。在该实现中调用json.Unmarshal(b, type)会导致堆栈溢出。

JSON解码器不断尝试查找是否有自定义的UnmarshalJSON实现,然后再次调用json.Unmarshal

有没有其他方法可以做到这一点?只调用底层的默认实现而不引起这个问题?

英文:

I wanted to perform some additional steps for initializing a data structure inside my implementation UnmarshalJSON. Calling json.Unmarshal(b, type) inside that implementation, naturally, causes a stack overflow.

The JSON decoder is continiously trying to look up, if there is a custom UnmarshalJSON implementation which then again, calls json.Unmarshal.

Is there another way to do this? Just call the underlying default implementation without causing this?

答案1

得分: 32

一种简单常见的避免这个问题或者保护自己免受其影响的方法是使用type关键字创建一个新类型,并使用类型转换来传递该类型的值(该值可以是原始值,因为新类型具有原始类型作为其基础类型)。

这种方法有效的原因是type关键字创建了一个新类型,而新类型将没有任何方法(它不会"继承"基础类型的方法)。

这会产生一些运行时开销吗?不会。引用自规范:转换

> 针对数值类型之间的(非常量)转换或者与字符串类型之间的转换,有特定的规则。这些转换可能会改变x的表示并产生运行时开销。所有其他转换只会改变类型而不会改变x的表示

让我们看一个例子。我们有一个具有数值类型AgePerson类型,并且我们希望确保Age不能为负数(小于0)。

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

func (p *Person) UnmarshalJSON(data []byte) error {
	type person2 Person
	if err := json.Unmarshal(data, (*person2)(p)); err != nil {
		return err
	}

    // 反序列化后的后处理:
	if p.Age < 0 {
		p.Age = 0
	}
	return nil
}

进行测试:

var p *Person
fmt.Println(json.Unmarshal([]byte(`{"name":"Bob","age":10}`), &p))
fmt.Println(p)

fmt.Println(json.Unmarshal([]byte(`{"name":"Bob","age":-1}`), &p))
fmt.Println(p)

输出结果(在Go Playground上尝试):

<nil>
&{Bob 10}
<nil>
&{Bob 0}

当然,相同的技术也适用于自定义编组(MarshalJSON()):

func (p *Person) MarshalJSON() ([]byte, error) {
	// 编组前的预处理:
	if p.Age < 0 {
		p.Age = 0
	}

	type person2 Person
	return json.Marshal((*person2)(p))
}

进行测试:

p = &Person{"Bob", 10}
fmt.Println(json.NewEncoder(os.Stdout).Encode(p))
p = &Person{"Bob", -1}
fmt.Println(json.NewEncoder(os.Stdout).Encode(p))

输出结果(在相同的Go Playground示例中):

{"name":"Bob","age":10}
<nil>
{"name":"Bob","age":0}
<nil>

当你为fmt包定义String() string方法以自定义文本表示时,并且你想要使用你修改后的默认字符串表示时,会出现非常类似的问题。在这里阅读更多相关信息:https://stackoverflow.com/questions/43065856/the-difference-between-t-and-t/43065979#43065979

英文:

An easy and common way to avoid this / protect from it is to create a new type with the type keyword, and use a type conversion to pass a value of this type (the value may be your original value, type conversion is possible because the new type has the original type as its underlying type).

This works because the type keyword creates a new type, and the new type will have zero methods (it does not "inherit" the methods of the underlying type).

Does this incur some run-time overhead? No. Quoting from Spec: Conversions:

> Specific rules apply to (non-constant) conversions between numeric types or to and from a string type. These conversions may change the representation of x and incur a run-time cost. All other conversions only change the type but not the representation of x.

Let's see an example. We have a Person type with a numeric Age, and we want to make sure the Age cannot be negative (less than 0).

type Person struct {
	Name string `json:&quot;name&quot;`
	Age  int    `json:&quot;age&quot;`
}

func (p *Person) UnmarshalJSON(data []byte) error {
	type person2 Person
	if err := json.Unmarshal(data, (*person2)(p)); err != nil {
		return err
	}

    // Post-processing after unmarshaling:
	if p.Age &lt; 0 {
		p.Age = 0
	}
	return nil
}

Testing it:

var p *Person
fmt.Println(json.Unmarshal([]byte(`{&quot;name&quot;:&quot;Bob&quot;,&quot;age&quot;:10}`), &amp;p))
fmt.Println(p)

fmt.Println(json.Unmarshal([]byte(`{&quot;name&quot;:&quot;Bob&quot;,&quot;age&quot;:-1}`), &amp;p))
fmt.Println(p)

Output (try it on the Go Playground):

&lt;nil&gt;
&amp;{Bob 10}
&lt;nil&gt;
&amp;{Bob 0}

Of course the same technique works for custom marshaling (MarshalJSON()) too:

func (p *Person) MarshalJSON() ([]byte, error) {
	// Pre-processing before marshaling:
	if p.Age &lt; 0 {
		p.Age = 0
	}

	type person2 Person
	return json.Marshal((*person2)(p))
}

Testing it:

p = &amp;Person{&quot;Bob&quot;, 10}
fmt.Println(json.NewEncoder(os.Stdout).Encode(p))
p = &amp;Person{&quot;Bob&quot;, -1}
fmt.Println(json.NewEncoder(os.Stdout).Encode(p))

Output (on the same Go Playground example):

{&quot;name&quot;:&quot;Bob&quot;,&quot;age&quot;:10}
&lt;nil&gt;
{&quot;name&quot;:&quot;Bob&quot;,&quot;age&quot;:0}
&lt;nil&gt;

A very similar issue is when you define the String() string method for custom text representation for the fmt package, and you want to use the default string representation which you modify. Read more about it here: https://stackoverflow.com/questions/43065856/the-difference-between-t-and-t/43065979#43065979

huangapple
  • 本文由 发表于 2017年4月3日 12:43:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/43176625.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定