原来 Go 里也可能会有二级指针的问题,神奇
背景
Update module mitchellh/mapstructure to 1.5.0 by StevenACoffman · Pull Request #2111 · 99designs/gql
How to fix shadowing introduced by decodeBasic in v1.3.2? · Issue #290 · mitchellh/mapstructure
将 mapstructure 包升级后出现错误,原来可以正常解析的变量在wrapCaller
中一切正常,但在调用它的函数中还是 nil
复现
func TestDecode(t *testing.T) {
var resp struct {
Text *string `mapstructure:"text"`
}
query := map[string]interface{}{
"text": "blah",
}
if err := wrapper(query, &resp); err != nil {
fmt.Printf("decode failed: %v", err)
return
}
fmt.Printf("after wrapper:\n%#v\n", resp)
}
func wrapper(query map[string]interface{}, r interface{}) error {
fmt.Printf("wrapper before unpack:\n%#v\n", r)
err := unpack(query, &r)
fmt.Printf("wrapper after unpack:\n%#v\n", r)
return err
}
func unpack(data interface{}, into interface{}) error {
d, err := NewDecoder(&DecoderConfig{
Result: into,
TagName: "json",
ErrorUnused: true,
ZeroFields: true,
})
if err != nil {
return fmt.Errorf("mapstructure: %w", err)
}
return d.Decode(data)
}
[email protected]
以后版本运行后得到以下结果
wrapper before unpack:
&struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(nil)}
wrapper after unpack:
&struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(0xc00004adf0)}
after wrapper:
struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(nil)}
[email protected]
及之前的版本得到的结果
wrapper before unpack:
&struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(nil)}
wrapper after unpack:
&struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(0xc00004add0)}
after wrapper:
struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(0xc00004add0)}
可以看到,更新后 after wrapper 中 Text 的变为了 nil
分析
调试运行,发现在wrapper
函数中出现了两个r
:参数r(shadowed)
和变量r
unpack
后变量r
的值正常,而参数r
的值还是 nil
显然参数r
的值才是上级函数中resp
的值。而在旧版中,依然会出现两个r
,但两个值都正常,因此没有出现问题。推测很有可能是 shadowed 导致unpack
只更新了变量r
。
-
shadowed 是什么?
shadowed,是由变量作用域中重新定义了一个同名变量,原变量被覆盖所导致的。以下代码无法得到预期值,正是因为变量i
在 if 中被覆盖了,无法修改原来的i
。
func TestShadowed(t *testing.T) {
i := 1
if true {
i := 1
i++
}
if i != 2 {
t.Errorf("expected 2, but got %d", i)
}
}
常见的 shadowed 还出现在返回值 err 被覆盖等情况中,goland 中会自动检测变量覆盖并以不同的颜色显示,严重时 go 在编译时也会给出错误提示。
-
什么导致了 shadowed?
在上面的例子中,出现 shadowed 的原因都是重新定义导致原变量被覆盖。然而,wrapper
函数中并没有定义任何新变量,显然参数r(shadowed)
另有原因。
猜测可能是resp
从*struct{}
转换成参数中interface{}
时 go 进行自动处理,但经过验证单纯的转换并不会导致shadowed
的出现,排除。
那么就只剩下err := caller(query, &r)
这一行了,把它注释掉后再运行,就只剩一个正常的 r 了,果然是它!
进一步分析发现,是因为&r
对 r 进行了取地址操作导致出现shadowed
。至此,我们已经得出了一个最简单的解决方法:将&
去掉,改为·err := caller(query, r)
。
-
为什么新版没有更新参数 r?
新版的改动:If interface value is not addressable, make copy and set · mitchellh/mapstructure@93663c4
大概就是对 interface,当实际变量的地址无法被获取时,意味着变量是不能更改的,创建一个相同类型的新变量进行替代。(旧版中强行更改那些不能更改的变量会 panic)
在我们的代码中wrapper
调用unpack
传入的参数&r
的类型是&(interface{} | *struct{...})
即interface{} | **struct{}
,Decoder.Decode()
中reflect.ValueOf(d.config.Result).Elem()
获取到的还是一个指针(*struct{}
),它的地址无法被获取。在旧版中,decode 会顺着指针做进一步解析。而在新版,因为无法获取地址,会直接创建一个新的指针再进行解析,原指针指向的变量(上级函数resp
)保持不变,引发 bug。
func TestDecode(t *testing.T) {
var resp struct {
Text *string `mapstructure:"text"`
}
query := map[string]interface{}{
"text": "blah",
}
if err := wrapper(query, &resp); err != nil {
fmt.Printf("decode failed: %v", err)
return
}
fmt.Printf("after wrapper:\n%#v\n", resp)
fmt.Printf(" address of resp: %p\n", &resp)
}
func wrapper(query map[string]interface{}, r interface{}) error {
fmt.Printf("wrapper before unpack:\n%#v\n", r)
fmt.Printf(" address of r: %p\n", &r)
fmt.Printf(" address of resp: %p\n", r)
err := unpack(query, &r)
fmt.Printf("wrapper after unpack:\n%#v\n", r)
fmt.Printf(" address of r: %p\n", &r)
fmt.Printf(" address of resp: %p\n", r)
return err
}
func unpack(data interface{}, into interface{}) error {
d, err := NewDecoder(&DecoderConfig{
Result: into,
TagName: "json",
ErrorUnused: true,
ZeroFields: true,
})
if err != nil {
return fmt.Errorf("mapstructure: %w", err)
}
return d.Decode(data)
}
wrapper before unpack:
&struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(nil)}
address of r: 0xc000104d50
address of resp: 0xc000140028
wrapper after unpack:
&struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(0xc000104dc0)}
address of r: 0xc000104d50
address of resp: 0xc000140050
after wrapper:
struct { Text *string "mapstructure:\"text\"" }{Text:(*string)(nil)}
address of resp: 0xc000140028
-
为什么会 shadowed?
而在wrapper
中,我们对一个指针进行了取地址操作,但指针可以直接取地址吗?reflect.ValueOf(r).CanAddr()
告诉我们不能。
在上一部分中,我们说到当变量地址无法被获取时,新版本会创建一个变量的 copy。猜测 go 中可能有类似的机制,在无法取地址时创建一个 copy 并把原来的变量覆盖掉。
使用go tool compile -S wrapper.go
查看 go 汇编代码,会发现用到了&r
的代码比没用到的复杂不少,而且有调用runtime.newobject
,说明 go 在取地址时确实会对无法取地址的变量创建 copy。
解决
如果要避免shadowed
,只要将wrapper
中&
去掉,改为·err := caller(query, r)
。
而对于mapstructure
,我们应该在decodeBasic
加入对指针的判断,避免对指针进行替换。