背景

在反序列化的时候,需要一个 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
}

举报· 309 次点击
登录 注册 站外分享
2 条回复  
PTLin 小成 3 天前
印象里 go 的命名返回值会带来一系列奇葩问题,在我眼里都属于语言层面的设计失误了,属于能不用就不用的东西。
Trim21 小成 3 天前
命名返回值确实挺奇葩,但这不是命名返回值带来的奇葩问题之一... 这里问题是问题是,你 doBug 里的 nil 指针是 copy 进去... Unmarshal 内部就算能 new 一个 Student 出来,他要怎么修改你 doBug 里面指针指向的值呢?
返回顶部