V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
Charlie17Li
V2EX  ›  Go 编程语言

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

  •  
  •   Charlie17Li · 2 天前 · 1261 次点击

    背景

    在反序列化的时候,需要一个 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
    }
    
    
    8 条回复    2025-03-23 16:29:58 +08:00
    PTLin
        1
    PTLin  
       2 天前
    印象里 go 的命名返回值会带来一系列奇葩问题,在我眼里都属于语言层面的设计失误了,属于能不用就不用的东西。
    Trim21
        2
    Trim21  
       2 天前
    命名返回值确实挺奇葩,但这不是命名返回值带来的奇葩问题之一...

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

    另外:

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

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

    豁然开朗,把最基本的原理给忽视了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   999 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 21:23 · PVG 05:23 · LAX 14:23 · JFK 17:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.