当反射 map[string]interface{} 遇上 MapIndex 方法,返回值的 Kind 不是具体类型?

19/Feb/2022 · 6 minute read

什么是反射?

反射是一种在运行时用于探测甚至修改内存数据以及程序行为的机制,在 go 语言中通过 reflect 包实现。直白来说,利用反射,我们可以实现包括但不限于的以下这些场景:

所以,这次想说什么问题呢?

今天想分享的,是我前几天在一个使用 golang 反射功能对 map[string]interface{} 类型的数据做处理的过程中,遇到的一个反直觉的问题。下面是相关代码片断示例:

myData := map[string]interface{}{}
json.Unmarshal("{\"name\": \"martin\", \"score\": 99}", &myData)

HandleData(myData) // 进行数据的处理过程

func HandleData(data interface{}) {
    value := reflect.ValueOf(data)
    // ... 其他代码

    keyValue := value.MapIndex(reflect.ValueOf("name")) // 从数据中取对应键 name 的值,应该为 "martin"
    switch keyValue.Kind() {
    case reflect.String:
        doSth()
    // ... 其他 case,但是都没有包含 reflect.Interface 的匹配
    }

    // ... 其他后续代码
}

在编写上面的代码的过程中,我期待程序会进入 case reflect.String: 的逻辑分支进行处理,但是事实上,并没有。在网上搜索了一番之后,StackOverflow 上的这个问答给出了可以奏效的方法:

keyValue := reflect.ValueOf(value.MapIndex(reflect.ValueOf("name")).Interface())

很奇怪啊! (╯‵□′)╯︵┻━┻
大家应该都知道,reflect.ValueOfreflect.Interface() 是一对相反的操作啊(看下图右侧部分),为什么要这么绕一圈,而且绕完还真的可以了?

test
图片来自《Go 语言设计与实现》博客,https://draveness.me/golang

出发之前,让我准备个小 demo

既然 map[string]interface{} 有这个奇怪问题,那 map[string]string 这种值类型确定的数据结构,是否就没有问题呢?来,上代码:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	implicitMap := map[string]interface{}{
		"name": "Martin",
	}
	explicitMap := map[string]string{
		"name": "Martin",
	}

	implicitRValue := reflect.ValueOf(implicitMap)
	explicitRValue := reflect.ValueOf(explicitMap)

	implicitNameValue := implicitRValue.MapIndex(reflect.ValueOf("name"))
	explicitNameValue := explicitRValue.MapIndex(reflect.ValueOf("name"))
	fmt.Printf("the kind of name key value in implicitRValue is: %s\n", implicitNameValue.Kind())
	fmt.Printf("the kind of name key value in explicitRValue is: %s\n", explicitNameValue.Kind())

	implicitNameInterface := implicitNameValue.Interface()
	fmt.Println("the type of implicitNameValue is: ", implicitNameValue.Type().String())

	if directName, ok := implicitNameInterface.(string); ok {
		fmt.Println("the directName is: ", directName)
	} else {
		fmt.Println("could not assert directName as string")
	}

	convertedImplicitNameValue := reflect.ValueOf(implicitNameInterface)
	fmt.Printf("the kind of name key value after converting is: %s\n", convertedImplicitNameValue.Kind())
	fmt.Println("the converted value is: ", convertedImplicitNameValue.Interface().(string))
}

demo 代码略多,大家不要怕。大概思路就是想看看两种不同值类型的 map 通过反射后的 MapIndex 方法得到的反射值的 Kind 有什么区别,以及,能否直接都进行类型转换。

怎么样?思路应该知道了吧?那你觉得这段程序的输出是什么呢?想好了的话,我们往下揭晓答案:

the kind of name key value in implicitRValue is: interface
the kind of name key value in explicitRValue is: string
the type of implicitNameValue is:  interface {}
the directName is:  Martin
the kind of name key value after converting is: string
the converted value is:  Martin

看到没有?

  1. map[string]interface{} 类型的数据对应的反射值在通过 MapIndex 方法获取到的值,对应的 Kind 是 interface
  2. map[string]string 类型的数据和1的数据尽管人类角度理解一致,但是其反射值在通过 MapIndex 方法获取到的值,对应的 Kind 是 string
  3. map[string]interface{} 类型的数据的反射值在通过 MapIndex 方法获取到的值,再经过一次 reflect.Interfacereflect.ValueOf 的往返后,最后的值的 Kind 成功变成 string 了!

我本来想玩一玩反射,结果没想到这是被反射玩了啊!到底是为什么?

开启原理分析之旅

提出问题

为了找出问题的答案,我把问题进行了分解,相信找到这些问题的答案之后,上面的问题就自然迎刃而解了。

既然提出来问题,我们就逐个问题击破吧,let’s go!

问题一:reflect.Value#Kind 方法如何工作,它怎么知道具体的 Kind 值?

要想知道,不妨跟着源码分析出发?以下是 Kind 方法的源码:

func (v Value) Kind() Kind {
	return v.kind()
}

有意思,它啥也不干,喊了自己的分身 kind 方法出来干活:

func (f flag) kind() Kind {
	return Kind(f & flagKindMask)
}

好家伙,这里也就一行代码,定睛一看,这里不是 Value 定义的方法,而是 Value 值的 flag 匿名字段的 kind 方法,干了啥呢?就是做了下位的掩码运算,翻了下代码,找到文件开头一处重点代码:

type flag uintptr

const (
	flagKindWidth        = 5 // there are 27 kinds
	flagKindMask    flag = 1<<flagKindWidth - 1
    //...

也就是说,上面 kind() 方法的实现逻辑就是取自身的 flag 值的低 5 位(也就是 0-31),而如果熟悉反射的同学都知道,reflect 包里总的定义了 27 个 kind 常量,其中 0 是非法 kind,我们刚才讨厌的 interface 对应的值是 20,map 是 21,我们想要的 string 是 24。

小结一个

所以到这里,我们的问题的答案就清楚了,Kind 方法实际只是从 reflect.Value 对象自身的 flag 字段的低 5 位中取出对应的 kind 值。那么,问题又变了:这个 flag 的低 5 位是怎么赋值的?

问题一的延伸:reflect.Value 对象的 flag 字段的 低 5 位,也就是 kind,是怎么来的?

想要解答这个问题,我找到了一点间接的线索。从《Go语言设计与实现》的 4.3.2 节中得到的下面 2 点结论需要大家先记住:

  1. 大家知道 reflect.ValueOf 函数的参数列表是 (i interface{}) Value,也就是入参是一个 interface{} 类型的值,但是,外部调用时,明明是 reflect.ValueOf(implicitMap),这里 implicitMap 明明只是一个 map[string]interface{} 类型的参数啊,为什么能工作?道理很简单,一切你没干,但是又能工作的语法,一定是编译器在背后为你默默做了一些事情。事实上,这里的类型转换就是编译器在编译阶段完成的。这个结论怎么证明呢?汇编!有关于这个结论的汇编代码展示和说明,感兴趣的可以自己去看,只记住结论就足以跟着我的脚步往下了。

  2. 进一步,Go 语言的 interface{} 类型在语言内部是通过 reflect.emptyInterface 结构体表示的:

    // emptyInterface is the header for an interface{} value.
    type emptyInterface struct {
        typ  *rtype
        word unsafe.Pointer
    }
    

    其中的 rtype 字段用于表示变量的类型,另一个 word 字段指向内部封装的数据。

接下来,我们回到这个问题,我们先看看当我们执行 reflect.ValueOf(xxx) 的时候,reflect.Value 的 flag 是怎么来的。依然从源码入手:

func ValueOf(i interface{}) Value {
	// 为了聚焦,此处省略一些不太重要的代码

	return unpackEface(i)
}

这里最重要的代码就这一行 unpackEface,其中 Eface 就是 Empty Interface,所以,unpack 了啥呢?

// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
	// NOTE: don't read e.word until we know whether it is really a pointer or not.
	t := e.typ
	if t == nil {
		return Value{}
	}
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}

嗯,可以,代码依旧不多。首先代码将空接口类型的值 i 通过指针强转转为了 emptyInterface 类型的值,还记得刚才说的第2点吗?

Go 语言的 interface{} 类型在语言内部是通过 reflect.emptyInterface 结构体表示

接着隔开5行,来到 f := flag(t.Kind()),也就是在这里,定下了 Value 的 kind 值。

我们不会止步于此,再看 *rtype#Kind 方法的源码:

const (
    // ...
    kindMask        = (1 << 5) - 1
    // ...
)

// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
	// 为了聚焦,此处省略一些不太重要的代码
	kind       uint8   // enumeration for C
    // ...
}

func (t *rtype) Kind() Kind { return Kind(t.kind & kindMask) }

这里又是一次掩码计算,kindMask 仍然是一个低5位的掩码,也就是 11111。而 rtype 类型中的 kind 数值中存着具体数据类型。

这里就能结合起来得到一个结论:

对于每个通过调用 reflect.ValueOf 函数得到的反射值,它的 Kind() 方法的结果取决于编译器在编译阶段实现的到 emptyInterface 类型的类型转换过程中存在 typ 字段指向的 rtype 类型的值中的 kind 字段的值。

有点拗口,画个图示意:

这套关系之后,我们里问题的真相更近一步了,只要我们知道:reflect.Value#MapIndex 方法又是怎么给返回的 Value 对象设置它的 flag 的呢?

你猜到了吗?继续看源码!

func (v Value) MapIndex(key Value) Value {
	tt := (*mapType)(unsafe.Pointer(v.typ))

	// 为了聚焦,此处省略一些不太重要的代码
	typ := tt.elem
	fl := (v.flag | key.flag).ro()
	fl |= flag(typ.Kind())
	return copyVal(typ, fl, e)
}

这里方法内部第一行的 v.typ 是怎么来的呢?回顾前面刚才看的 unpackEface 函数,同时结合 Value 的定义:

func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
	t := e.typ
    // 为了聚焦,此处省略一些不太重要的代码
	return Value{t, e.word, f}
}

type Value struct {
	// typ holds the type of the value represented by a Value.
	typ *rtype
    // 为了聚焦,此处省略一些不太重要的代码
	flag
}

所以,这里的 v.typ 就是表示变量的类型,它的类型是 rtype。回到 MapIndex 的代码,tt 是将 rtype 类型的值转换为 mapType 类型的值,其中 mapType 的定义就是:

// mapType represents a map type.
type mapType struct {
	rtype
	key    *rtype // map key type
	elem   *rtype // map element (value) type
    // 为了聚焦,此处省略一些不太重要的代码
}

其中的 key 代表键值对中键的类型,而 elem 代表键值对中值的类型。继续回到 MapIndex 的剩余代码:

    typ := tt.elem
	fl := (v.flag | key.flag).ro()
	fl |= flag(typ.Kind())
	return copyVal(typ, fl, e)

    func copyVal(typ *rtype, fl flag, ptr unsafe.Pointer) Value {
        // 为了聚焦,此处省略一些不太重要的代码
        return Value{typ, *(*unsafe.Pointer)(ptr), fl}
    }

这里的 fl 通过 fl |= flag(typ.Kind()) 加上了复制了 typkind,也就是键值对中值的类型的 kind

了解了这些原理之后,我们可以自然想到,回到我们的 demo,那我们看下 implicitRValueexplicitRValue 分别对应的 typelem 的值不就知道为什么后面的 MapIndex 方法返回的 Value 对象的 Kind() 为什么不同了?以下上个调试截图:

问题一总结

  1. reflect.ValueKind 方法返回的是自身 flag 字段的低 5 位表示的枚举值 kind;
  2. reflect.ValueMapIndex 方法返回的新的 Value 对象的 flag 的 kind 是原 Value 对象的值类型的 kind,对于 reflect.ValueOf(map[string]interface{}) 的值,它的值类型的 kind 是 20,即 interface;而 对于 reflect.ValueOf(map[string]interface{}) 的值,它的值类型的 kind 是 24, 即 string。

问题二:在 reflect.Value#Interfacereflect.ValueOf 这一来一往之间,是哪个操作起了关键作用,让反射值的 Kind 得到纠正?

我们不妨先看看 Interface 方法如何工作:

func (v Value) Interface() (i interface{}) {
	return valueInterface(v, true)
}

func valueInterface(v Value, safe bool) interface{} {
	// 为了聚焦,此处省略一些不太重要的代码

	if v.kind() == Interface {
		// Special case: return the element inside the interface.
		// Empty interface has one layout, all interfaces with
		// methods have a second layout.
		if v.NumMethod() == 0 {
			return *(*interface{})(v.ptr)
		}
		return *(*interface {
			M()
		})(v.ptr)
	}

	// TODO: pass safe to packEface so we don't need to copy if safe==true?
	return packEface(v)
}

在问题一的分析中,我们可以确认 reflect.ValueOf(map[string]interface{}) 的值调用 MapIndex 会得到的 ValueKind() 会是 Interface,于是我们的 demo 会走到 return *(*interface{})(v.ptr) 这一行。其中 v.ptr 的定义是:

type Value struct {
	// typ holds the type of the value represented by a Value.
	typ *rtype

	// Pointer-valued data or, if flagIndir is set, pointer to data.
	// Valid when either flagIndir is set or typ.pointers() is true.
	ptr unsafe.Pointer

也就是说,它指向反射值所包装的真实数据。所以,reflect.Value 对象的 Interface() 会返回实际的数据。

开始意识到什么没有?如果我们再将这个实际的数据传给 reflect.ValueOf() 函数,会发生什么?回想一下!前面已经分析过了:

对于每个通过调用 reflect.ValueOf 函数得到的反射值,它的 Kind() 方法的结果取决于编译器在编译阶段实现的到 emptyInterface 类型的类型转换过程中存在 typ 字段指向的 rtype 类型的值中的 kind 字段的值。

想不起来的,返回去看看前面的图片。

问题二的总结

  1. reflect.ValueOf(xxx.Interface()) 通过先获取真实的数据再转回反射值,从而能够通过 Kind 拿到真实数据的实际类型,而不是在 MapIndex 过程中复制的 kind

总结

  1. 通过 reflect.Value#Interface() 方法,我们获得了实际的数据的空接口表示,而再用 reflect#ValueOf 函数将空接口转成反射值,就可以利用 Kind() 获取到真实数据的实际类型了;
  2. 除了字典类型的 MapIndex 会有上述问题,同理列表型数据的 Index(i int) 方法也有相同问题;
  3. 在阅读反射源码的过程中,看到了一些理论上影响 golang 运行时性能的源码,比如 ValueOf 函数会先将变量逸出到堆等,后面可以再写写关于反射带来的一些问题。

参考资料

版权声明:本文为原创文章,转载请注明来源:《当反射 map[string]interface{} 遇上 MapIndex 方法,返回值的 Kind 不是具体类型? - Hackerpie》,谢绝未经允许的转载。