Golang不完全の笔记(一):反射


​ 反射这个概念比较常见了,在其他语言中也有反射这个概念(不过还有的语言不支持), Java中利用反射可以创建某个对象对应类的实例、访问成员变量和调用方法、动态创建Array数组等。在go中我们可以利用反射主要可以解决以下几件事情:

  • 不知道函数的参数是什么,或者入参类型很多,没有办法统一表示,那就用反射;

  • 根据某些条件决定调用不同的函数,那就根据函数和函数的参数进行反射。

​ 但是在实际的工程代码里,比较少看到运用反射去做什么事情,首先是反射相关的代码不太好阅读以及比代码运行速度要慢一些还有使用不当会引发panic,但是我们身边的一些开源库中还是存在应用反射的例子:

  • 实现go语言中的数据类型与文本/二进制数据进行转换,其实就是通过反射完成序列化和反序列化。

    func Marshal(v interface{}) ([]byte, error)
    func Unmarshal(data []byte, v interface{}) error
    
  • 很多时候我们在部署服务的时候需要去读取一个yaml类型的配置文件,那么通过反射我们可以去解析获得的配置文件。

  • 针对go语言,有一些不错的orm框架,使用它们的时候我们需要自己去定义结构体,orm框架做的事情就是把model和要操作的数据库表对应起来,如何实现映射呢,其实也用到了反射。

  • 在go中判断两个map的key-value是否完全相同的时候,需要用到一个函数DeepEqual。在之前的java中要去判断key是否相同呢,我们需要利用key对应的hash值以及通过==判断key的内容是否相同,但是go就没有java这么花里胡哨, 那么在DeepEqual是怎么做的呢,就用到了反射,所以可以看到这个函数实在reflect包下面的。下面源码中的ValueOf()其实就是用到反射的体现。

    func DeepEqual(x, y interface{}) bool {
    	if x == nil || y == nil {
    		return x == y
    	}
    	v1 := ValueOf(x)
    	v2 := ValueOf(y)
    	if v1.Type() != v2.Type() {
    		return false
    	}
    	return deepValueEqual(v1, v2, make(map[visit]bool), 0)
    }
    

    go有一个地方需要注意下,它有底层类型和静态类型之说(后面提到kind和type的时候举了个例子),结合deepequal函数举个例子说明一下。两个变量的静态类型一个是MyInt,一个是YourInt,虽然底层类型都是int,值也都是1,但我们并不认为这样的两个变量是相等的。判断相等具体标准,可以点我

    type MyInt int
    type YourInt int
    
    func main() {
    	m := MyInt(1)
    	y := YourInt(1)
    
    	fmt.Println(reflect.DeepEqual(m, y)) // false
    }
    

​ 到此为止,大概对之前语言中的反射和Go中的反射能干啥做了一个简单的总结。接下来看看go是怎么实现的反射,其实主要围绕三个概念:Type,Kind和Value,反射的各种功能依赖标准库中的reflect包实现。

reflect包

reflect包里定义了一个接口和一个结构体,即reflect.Typereflect.Value,里面提供了很多函数获取存储在接口中的类型信息。

​ 下面两个函数我们可以分别用来获取接口和结构体。

func TypeOf(i interface{}) Type 
func ValueOf(i interface{}) Value

TypeOf

​ 先说TypeOf函数,在源码中该函数的输入参数是一个空接口interface{},所以我们可以知道实参会被转为空接口类型,实参的类型信息、方法和值都会存储到这个空接口当中。

func TypeOf(i interface{}) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
    //eface.type指的是动态类型
	return toType(eface.typ)
}

​ 可以看到在将i转化为空接口的时候出现了emptyInterface(reflect包中),这个类型和efaceruntime包中)其实是差不多的。不过还没有总结接口,所以efaceiface相关的内容还不太了解,等后面补一下。

​ 书接上文,返回结果的时候通过toType函数做了一个类型转换的工作,返回值Type是一个接口(定义获取类型相关的信息的若干方法),入参类型*rtype实现了该接口,点开toType方法的源码看一下:

func toType(t *rtype) Type {
	if t == nil {
		return nil
	}
	return t
}

​ 在Go语言圣经中讲反射的部分提到了reflect.Type接口时满足fmt.Stringer接口的,这是为什么呢。因为Type接口实现了String()函数。还可以使用%T作为格式参数输出动态类型reflect.TypeOf的结果。

​ 下面对于使用TypeOf举了几个例子,不过还是要说明一下,该函数返回的一个动态类型的接口值,返回的是具体的类型。

type cat int

func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v\n", v)
}

func main() {
	var a float32 = 3.14
	reflectType(a) // type:float32
	var b int64 = 100
	reflectType(b) // type:int64
	var animal cat
	reflectType(animal)//type:main.cat
}
type name和type kind

​ 在反射中关于类型还划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

package main

import (
	"fmt"
	"reflect"
)

type myInt int64

func reflectType(x interface{}) {
	t := reflect.TypeOf(x)
	fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}

func main() {
	var a *float32 // 指针
	var b myInt    // 自定义类型
	var c rune     // 类型别名
	reflectType(a) // type: kind:ptr
	reflectType(b) // type:myInt kind:int64
	reflectType(c) // type:int32 kind:int32

	type person struct {
		name string
		age  int
	}
	type book struct{ title string }
	var d = person{
		name: "葫芦娃",
		age:  18,
	}
	var e = book{title: "《资治通鉴》"}
	reflectType(d) // type:person kind:struct
	reflectType(e) // type:book kind:struct
}

​ Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回

ValueOf

reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。

​ 下面是相关的源码:

func ValueOf(i interface{}) Value {
	if i == nil {
		return Value{}
	}
	
   // ……
	return unpackEface(i)
}

func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))

	t := e.typ
	if t == nil {
		return Value{}
	}
	
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}

​ 在unpackEface()拆分的时候,最终返回的是一个Value类型的结构体,这个结构体里面有啥呢,它包含了类型结构体指针、真实数据的地址以及标志位。Value类型的结构体本身也含有非常多的方法,可以直接操作指针指向的实际数据。

​ 在讲ValueOf的开头部分,提到了这样一句话:reflect.Value与原始值之间可以互相转换。下面研究一下,这个是怎么做到的。

三者关系

​ 通过 Type() 方法和 Interface() 方法可以打通 interfaceTypeValue 三者。Type() 方法也可以返回变量的类型信息,与 reflect.TypeOf() 函数等价。Interface() 方法可以将 Value 还原成原来的 interface。

通过反射获取值
func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	k := v.Kind()
	switch k {
	case reflect.Int64:
		// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
		fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
	case reflect.Float32:
		// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
		fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
	case reflect.Float64:
		// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
		fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
	}
}
func main() {
	var a float32 = 3.14
	var b int64 = 100
	reflectValue(a) // type is float32, value is 3.140000
	reflectValue(b) // type is int64, value is 100
	// 将int类型的原始值转换为reflect.Value类型
	c := reflect.ValueOf(10)
	fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}
通过反射设置变量的值

​ 想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值(使用指针去解决)。而反射中使用专有的Elem()方法来获取指针对应的值。

package main

import (
	"fmt"
	"reflect"
)

func reflectSetValue1(x interface{}) {
	v := reflect.ValueOf(x)
	if v.Kind() == reflect.Int64 {
		v.SetInt(200) //修改的是副本,reflect包会引发panic
	}
}
func reflectSetValue2(x interface{}) {
	v := reflect.ValueOf(x)
	// 反射中使用 Elem()方法获取指针对应的值
	if v.Elem().Kind() == reflect.Int64 {
		v.Elem().SetInt(200)
	}
}
func main() {
	var a int64 = 100
	// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
	reflectSetValue2(&a)
	fmt.Println(a)
}

​ 说到这里要提一下Go官方博客中的反射三大定律:

1.Reflection goes from interface value to reflection object.

2.Reflection goes from reflection object to interface value.

3.To modify a reflection object, the value must be settable.

​ 前两条还比较好理解,通过TypeOfValueOf函数可以实现第一条,实现第二条就可以用前面提到的Value类型结构体中的interface()方法实现逆转化。

​ 第三条耗尽洪荒之力可能也不太好理解了,典型明明英文读的懂就是不知道它在说啥。官方啥意思呢,先翻译一下就是你要是想操作反射变量,那么这个值必须是可设置的。啥叫可设置的呢,我们原来有个变量A(就是原变量),然后通过反射拿到了它对应的反射变量B,我们对变量B的修改同时会影响原变量A,那么这就属于可设置的。

​ 说的高级一点就是,reflect.ValueOf(A)只做了一个浅拷贝的工作,如果要对值进行操作,反射变量和原变量都需要改,否则只对反射变量(副本)做修改是不被允许的。

函数反射

​ 不知道函数的参数是什么,或者入参类型很多,没有办法统一表示,那就用反射;

package main

import (
	"reflect"
	"fmt"
)

type Child struct {
	Name     string
	Grade    int
	Handsome bool
}

type Adult struct {
	ID         string `qson:"Name"`
	Occupation string
	Handsome   bool
}

// 如果输入参数 i 是 Slice,元素是结构体,有一个字段名为 `Handsome`,
// 并且有一个字段的 tag 或者字段名是 `Name` ,
// 如果该 `Name` 字段的值是 `qcrao`,
// 就把结构体中名为 `Handsome` 的字段值设置为 true。
func handsome(i interface{}) {
	// 获取 i 的反射变量 Value
	v := reflect.ValueOf(i)

	// 确定 v 是一个 Slice
	if v.Kind() != reflect.Slice {
		return
	}

	// 确定 v 是的元素为结构体
	if e := v.Type().Elem(); e.Kind() != reflect.Struct {
		return
	}

	// 确定结构体的字段名含有 "ID" 或者 json tag 标签为 `name`
	// 确定结构体的字段名 "Handsome"
	st := v.Type().Elem()

	// 寻找字段名为 Name 或者 tag 的值为 Name 的字段
	foundName := false
	for i := 0; i < st.NumField(); i++ {
		f := st.Field(i)
		tag := f.Tag.Get("qson")

		if (tag == "Name" || f.Name == "Name") && f.Type.Kind() == reflect.String {
			foundName = true
			break
		}
	}

	if !foundName {
		return
	}

	if niceField, foundHandsome := st.FieldByName("Handsome"); foundHandsome == false || niceField.Type.Kind() != reflect.Bool {
		return
	}

	// 设置名字为 "qcrao" 的对象的 "Handsome" 字段为 true
	for i := 0; i < v.Len(); i++ {
		e := v.Index(i)
		handsome := e.FieldByName("Handsome")

		// 寻找字段名为 Name 或者 tag 的值为 Name 的字段
		var name reflect.Value
		for j := 0; j < st.NumField(); j++ {
			f := st.Field(j)
			tag := f.Tag.Get("qson")

			if tag == "Name" || f.Name == "Name" {
				name = v.Index(i).Field(j)
			}
		}

		if name.String() == "qcrao" {
			handsome.SetBool(true)
		}
	}
}

func main() {
	children := []Child{
		{Name: "Ava", Grade: 3, Handsome: true},
		{Name: "qcrao", Grade: 6, Handsome: false},
	}

	adults := []Adult{
		{ID: "Steve", Occupation: "Clerk", Handsome: true},
		{ID: "qcrao", Occupation: "Go Programmer", Handsome: false},
	}

	fmt.Printf("adults before handsome: %v\n", adults)
	handsome(adults)
	fmt.Printf("adults after handsome: %v\n", adults)

	fmt.Println("-------------")

	fmt.Printf("children before handsome: %v\n", children)
	handsome(children)
	fmt.Printf("children after handsome: %v\n", children)
}

output :

adults before handsome: [{Steve Clerk true} {qcrao Go Programmer false}]
adults after handsome: [{Steve Clerk true} {qcrao Go Programmer true}]
-------------
children before handsome: [{Ava 3 true} {qcrao 6 false}]
children after handsome: [{Ava 3 true} {qcrao 6 true}]

结构体反射

与结构体相关的方法

​ 任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。

StructField类型

StructField类型用来描述结构体中的一个字段的信息。

StructField的定义如下:

type StructField struct {
    // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
    // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
    Name    string
    PkgPath string
    Type      Type      // 字段的类型
    Tag       StructTag // 字段的标签
    Offset    uintptr   // 字段在结构体中的字节偏移量
    Index     []int     // 用于Type.FieldByIndex时的索引切片
    Anonymous bool      // 是否匿名字段
}

结构体反射示例

​ 当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。

type student struct {
	Name  string `json:"name"`
	Score int    `json:"score"`
}

func main() {
	stu1 := student{
		Name:  "葫芦娃",
		Score: 90,
	}

	t := reflect.TypeOf(stu1)
	fmt.Println(t.Name(), t.Kind()) // student struct
	// 通过for循环遍历结构体的所有字段信息
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
	}

	// 通过字段名获取指定结构体字段信息
	if scoreField, ok := t.FieldByName("Score"); ok {
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
	}
}

​ 接下来编写一个函数printMethod(s interface{})来遍历打印s包含的方法。

// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
	msg := "好好学习,天天向上。"
	fmt.Println(msg)
	return msg
}

func (s student) Sleep() string {
	msg := "好好睡觉,天天长个。"
	fmt.Println(msg)
	return msg
}

func printMethod(x interface{}) {
	t := reflect.TypeOf(x)
	v := reflect.ValueOf(x)

	fmt.Println(t.NumMethod())
	for i := 0; i < v.NumMethod(); i++ {
		methodType := v.Method(i).Type()
		fmt.Printf("method name:%s\n", t.Method(i).Name)
		fmt.Printf("method:%s\n", methodType)
		// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
		var args = []reflect.Value{}
		v.Method(i).Call(args)
	}
}

反射在GORM中的运用

​ 选了一个用过的ORM框架,看看在GORM中都干了写啥事情,下面的代码主要是围绕从数据库中查询数据。简单说一下都干了啥:

  • queryCallback 方法组成sql语句,调用database/sql 包中的query来查询,循环rows结果通过反射组成对象。
//callback_query.go
func queryCallback(scope *Scope) {
    if _, skip := scope.InstanceGet("gorm:skip_query_callback"); skip {
        return
    }

    //we are only preloading relations, dont touch base model
    if _, skip := scope.InstanceGet("gorm:only_preload"); skip {
        return
    }

    defer scope.trace(NowFunc())

    var (
        isSlice, isPtr bool
        resultType     reflect.Type
        results        = scope.IndirectValue()
    )
    // 找到排序字段
    if orderBy, ok := scope.Get("gorm:order_by_primary_key"); ok {
        if primaryField := scope.PrimaryField(); primaryField != nil {
            scope.Search.Order(fmt.Sprintf("%v.%v %v", scope.QuotedTableName(), scope.Quote(primaryField.DBName), orderBy))
        }
    }

    if value, ok := scope.Get("gorm:query_destination"); ok {
        results = indirect(reflect.ValueOf(value))
    }

    if kind := results.Kind(); kind == reflect.Slice {
        isSlice = true
        resultType = results.Type().Elem()
        results.Set(reflect.MakeSlice(results.Type(), 0, 0))

        if resultType.Kind() == reflect.Ptr {
            isPtr = true
            resultType = resultType.Elem()
        }
    } else if kind != reflect.Struct {
        scope.Err(errors.New("unsupported destination, should be slice or struct"))
        return
    }
    // 准备查询语句
    scope.prepareQuerySQL()

    if !scope.HasError() {
        scope.db.RowsAffected = 0
        if str, ok := scope.Get("gorm:query_option"); ok {
            scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str))
        }
        // 调用database/sql 包中的query来查询
        if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
            defer rows.Close()

            columns, _ := rows.Columns()
            // 循环rows 组成对象
            for rows.Next() {
                scope.db.RowsAffected++

                elem := results
                if isSlice {
                    elem = reflect.New(resultType).Elem()
                }

                scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields())

                if isSlice {
                    if isPtr {
                        //通过反射组成新的slice
                        results.Set(reflect.Append(results, elem.Addr()))
                    } else {
                        results.Set(reflect.Append(results, elem))
                    }
                }
            }

            if err := rows.Err(); err != nil {
                scope.Err(err)
            } else if scope.db.RowsAffected == 0 && !isSlice {
                scope.Err(ErrRecordNotFound)
            }
        }
    }
}

//scope.go
func (scope *Scope) prepareQuerySQL() {
    // 如果是原生语句 则组织sql语句
    if scope.Search.raw {
        scope.Raw(scope.CombinedConditionSql())
    } else {
        // 组织select 语句
        // scope.selectSQL() 组织select 需要查询的字段
        // scope.QuotedTableName() 获取表名
        // scope.CombinedConditionSql()组织条件语句
        scope.Raw(fmt.Sprintf("SELECT %v FROM %v %v", scope.selectSQL(), scope.QuotedTableName(), scope.CombinedConditionSql()))
    }
    return
}

为什么我们吐槽反射太慢

​ 在一些博客中看到这样一段话(大意是反射效率不高),好奇宝宝的我照着Golang反射性能分析这篇博客简单测了一下,确实反射挺慢…

反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

小结

1.开篇的时候提到了不推荐使用reflect的原因中,有一条是容易发生panic,下面举几个容易踩坑的例子:

// 设置切片的 len 字段,如果类型不是切片,就会panic
func (v Value) SetLen(n int)
//判断v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一,否则IsNil函数会导致panic。
func (v Value) IsNil() bool

​ 开头提到的深度相等函数,也是属于效率比较慢的,如果其他的判断方法无法满足要求,可以试一下深度相等大杀器。

2.通过看源码可以发现,基本上反射可以接受任意的interface{}类型,而对于interface{}其实也是需要去花点时间看看的,否则就像我一样一开始分不清efaceiface.

3.看到有的博客里说反射有点慢,但实际上前几天我做了一个结构体转map的测试,分别用json和反射实现的,反射就快很多,现在看来可能和其他方法比反射算快的也算慢的.写反射相关的总结的时候总算记得测试一下反射性能了.反射为什么慢呢.以给结构体的变量赋值为例.反射方法FieldByName(“B”)要去遍历结构体的字段,一一进行匹配找到我们需要的field,所以这么折腾就慢了好多.学到了…

for i := 0; i < b.N; i++ {
    //通过FieledByName找到对应Field赋值
    r = temp.FieldByName("B").SetInt(int64(2008))
}
for i := 0; i < b.N; i++ {
    	//直接找到对应field赋值
		tf.B= i
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值