
在反序列化的时候,需要一个 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) } 于是有了几个疑问:
通过分析 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 完整的 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 } 1 PTLin 2025 年 3 月 22 日 印象里 go 的命名返回值会带来一系列奇葩问题,在我眼里都属于语言层面的设计失误了,属于能不用就不用的东西。 |
2 Trim21 2025 年 3 月 22 日 命名返回值确实挺奇葩,但这不是命名返回值带来的奇葩问题之一... 这里问题是问题是,你 doBug 里的 nil 指针是 copy 进去... Unmarshal 内部就算能 new 一个 Student 出来,他要怎么修改你 doBug 里面指针指向的值呢? |
3 leonshaw 2025 年 3 月 22 日 参数是传值的,函数无法改变传入变量本身的值。 另外: > return s, json.Unmarshal(data, &s) 不要这样写,求值顺序有问题 https://groups.google.com/g/golang-nuts/c/Q7KVGTFt3nU/m/WgnbugtwDAAJ |
4 ninjashixuan 2025 年 3 月 22 日 div class="reply_content">基本不用命名返回值,除非是在 defer 里用到才会考虑。 |
5 liyunlong41 2025 年 3 月 23 日 via iPhone 用正常写法,别玩花哨的,为你好也为别人好。 |
6 voidmnwzp 2025 年 3 月 23 日 via iPhone unmarshal 调用方必须传入指针类型是因为 unmarshal 内部需要写入指针对应的内存,这样反序列化完成后,调用能找到对应的内存,而 nil 不指向任何内存 |
7 lovelylain 2025 年 3 月 23 日 via Android return s, json.Unmarshal(data, s) 值传递,你传个 nil 的 s 进去,就算内部给你 new 了一个对象,也没法影响你这个 s 还是 nil |
8 Charlie17Li OP |