[Golang] 反序列化中的隐式转换设计问题讨论

192 天前
 Charlie17Li

背景

在反序列化的时候,需要一个 interface 用来接收反序列化的内容,如下代码所示

func doNormal(data []byte) (*Student, error) {
    s := &Student{}
    if err := json.Unmarshal(data, s); err != nil {
    	return nil, err
    }
    return s, nil
}

然而为了使代码更加简化,习惯性得使用了命名返回值,导致反序列化失败

func doBug(data []byte) (s *Student, _ error) {
    return s, json.Unmarshal(data, s)
}

原因是因为 json.Unmarshal 中发现 s 是一个 nil ,直接 return error, 但将参数改成二级指针就 work 了

func doSimple(data []byte) (s *Student, _ error) {
    return s, json.Unmarshal(data, &s)
}

于是有了几个疑问:

  1. 这里二级指针能 work 的原因是啥?
  2. 这里二级指针指向的也是一个 nil 的一级指针,它能 work ,为啥直接使用一个 nil 的一级指针不行,这样设计的原因是啥?

开始分析

通过分析 json.Unmarshal 的代码可以发现 indirect 函数,这个函数会将二级指针反解成一级指针,并且发现一级指针是 nil 的时候,会初始化一个 Student

大概过程是如下所示

var p * Student
var pp ** Student = &p

v := reflect.ValueOf(pp).Elem() // v is nil

v.Set(reflect.New(v.Type().Elem())) // v is not nil

问题

  1. 使用二级指针的方式还有啥坑吗,大家是怎么简化反/序列化代码的呢?
  2. 这里二级指针指向的也是一个 nil 的一级指针,而直接使用一个 nil 的一级指针就直接报错,这样设计的原因是啥?

完整的 indirect 代码如下

// indirect walks down v allocating pointers as needed,
// until it gets to a non-pointer.
// If it encounters an Unmarshaler, indirect stops and returns that.
// If decodingNull is true, indirect stops at the first settable pointer so it
// can be set to nil.
func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) {
	// Issue #24153 indicates that it is generally not a guaranteed property
	// that you may round-trip a reflect.Value by calling Value.Addr().Elem()
	// and expect the value to still be settable for values derived from
	// unexported embedded struct fields.
	//
	// The logic below effectively does this when it first addresses the value
	// (to satisfy possible pointer methods) and continues to dereference
	// subsequent pointers as necessary.
	//
	// After the first round-trip, we set v back to the original value to
	// preserve the original RW flags contained in reflect.Value.
	v0 := v
	haveAddr := false

	// If v is a named type and is addressable,
	// start with its address, so that if the type has pointer methods,
	// we find them.
	if v.Kind() != reflect.Pointer && v.Type().Name() != "" && v.CanAddr() {
		haveAddr = true
		v = v.Addr()
	}
	for {
		// Load value from interface, but only if the result will be
		// usefully addressable.
		if v.Kind() == reflect.Interface && !v.IsNil() {
			e := v.Elem()
			if e.Kind() == reflect.Pointer && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Pointer) {
				haveAddr = false
				v = e
				continue
			}
		}

		if v.Kind() != reflect.Pointer {
			break
		}

		if decodingNull && v.CanSet() {
			break
		}

		// Prevent infinite loop if v is an interface pointing to its own address:
		//     var v interface{}
		//     v = &v
		if v.Elem().Kind() == reflect.Interface && v.Elem().Elem() == v {
			v = v.Elem()
			break
		}
		if v.IsNil() {
			v.Set(reflect.New(v.Type().Elem()))
		}
		if v.Type().NumMethod() > 0 && v.CanInterface() {
			if u, ok := v.Interface().(Unmarshaler); ok {
				return u, nil, reflect.Value{}
			}
			if !decodingNull {
				if u, ok := v.Interface().(encoding.TextUnmarshaler); ok {
					return nil, u, reflect.Value{}
				}
			}
		}

		if haveAddr {
			v = v0 // restore original value after round-trip Value.Addr().Elem()
			haveAddr = false
		} else {
			v = v.Elem()
		}
	}
	return nil, nil, v
}

2179 次点击
所在节点    Go 编程语言
8 条回复
PTLin
192 天前
印象里 go 的命名返回值会带来一系列奇葩问题,在我眼里都属于语言层面的设计失误了,属于能不用就不用的东西。
Trim21
192 天前
命名返回值确实挺奇葩,但这不是命名返回值带来的奇葩问题之一...

这里问题是问题是,你 doBug 里的 nil 指针是 copy 进去... Unmarshal 内部就算能 new 一个 Student 出来,他要怎么修改你 doBug 里面指针指向的值呢?
leonshaw
192 天前
参数是传值的,函数无法改变传入变量本身的值。

另外:

> return s, json.Unmarshal(data, &s)

不要这样写,求值顺序有问题 https://groups.google.com/g/golang-nuts/c/Q7KVGTFt3nU/m/WgnbugtwDAAJ
ninjashixuan
192 天前
基本不用命名返回值,除非是在 defer 里用到才会考虑。
liyunlong41
192 天前
用正常写法,别玩花哨的,为你好也为别人好。
voidmnwzp
192 天前
unmarshal 调用方必须传入指针类型是因为 unmarshal 内部需要写入指针对应的内存,这样反序列化完成后,调用能找到对应的内存,而 nil 不指向任何内存
lovelylain
192 天前
return s, json.Unmarshal(data, s) 值传递,你传个 nil 的 s 进去,就算内部给你 new 了一个对象,也没法影响你这个 s 还是 nil
Charlie17Li
191 天前
@Trim21
@voidmnwzp
@lovelylain
@leonshaw

豁然开朗,把最基本的原理给忽视了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1120344

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX