前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang之reflect

Golang之reflect

作者头像
LinkinStar
发布2022-09-01 13:57:28
4750
发布2022-09-01 13:57:28
举报
文章被收录于专栏:LinkinStar's Blog

反射 —— 如果你之前学过一些别的语言,比如java可能就会了解,反射是一个传说中很厉害的操作,算是一个高级用法。而同时,很多人也会告诉你,反射是一个危险的操作,那么在golang中,反射又是怎么操作的呢?今天就来说说golang中的反射reflect。

反射的定义

首先问问自己,你知道什么是反射吗?如果你有一个清楚的定义,证明你已经对反射非常熟悉了。 官方的定义很官方,我就说说我的: 反射,反射,从字面理解就是通过镜子(或类似的东西)看到自己。 而在编程中,反射指的是在运行的过程中看到自己。 在实际的编程过程中我们知道,创建的这个变量或者对象是什么类型或者是什么样子的,同时很容易能对它进行操作。而在运行过程中,程序没有我们的眼睛,它并不知道这个东西是怎么样的,这个时候就需要运用到反射。 通过反射我可以知道自己长什么样子。

反射的使用

reflect.TypeOf

如果你对反射还是有些模糊,那么看下面这个最简单的例子

代码语言:javascript
复制
func main() {
    a := 1.3
    fmt.Println("a的类型是", reflect.TypeOf(a))
}

输出

代码语言:javascript
复制
a的类型是 float64

是不是瞬间明白了,没错,反射没有那么复杂。

你想想,作为程序自己,我运行中,我怎么知道a是什么类型,只能通过照镜子(反射)得到。

下面再说说一些更高级的用法。

reflect.ValueOf

代码语言:javascript
复制
func main() {
    type MyInt int
    var x MyInt = 7
    v := reflect.ValueOf(x)
    fmt.Println(v.Type())
    fmt.Println(v.Kind())
}

输出

代码语言:javascript
复制
main.MyInt
int

这里我们通过reflect.ValueOf方法拿到的v,其中v.Type()拿到的是它当前的类型,而v.Kind()可以拿到它最基本的类型。

Elem()

代码语言:javascript
复制
func main() {
    a := 1.3
    v := reflect.ValueOf(&a)
    elem := v.Elem()
    elem.SetFloat(0.2)
    fmt.Println(a)
}

输出

代码语言:javascript
复制
0.2

这里我们可以看到,通过反射拿到v使用v.Elem()方法可以拿到对应指针进行操作赋值

Field()

代码语言:javascript
复制
type MyData struct {
    A int
    b float32
} 

func main() {
    myData := MyData{
        A: 1,
        b: 1.1,
    }
    myDataV := reflect.ValueOf(&myData).Elem()
    fmt.Println("字段a:", myDataV.Field(0))
    fmt.Println("字段b:", myDataV.Field(1))
    fmt.Println(myDataV)
    myDataV.Field(0).SetInt(2)
    fmt.Println(myDataV)
}

输出

代码语言:javascript
复制
字段a: 1
字段b: 1.1
{1 1.1}
{2 1.1}

这里我们可以看到,我们即使不知道一个结构体里面的情况,我们依旧可以通过Field方法获得其中的值,并且如果这个变量可以被外界访问那么还可以修改。

Interface()

代码语言:javascript
复制
func main() {
    a := 1.3
    v := reflect.ValueOf(a)
    a1 := v.Interface().(float64)
    fmt.Println(a1)
}

1.3

代码语言:javascript
复制
a的类型是 float64

反射之后的对象通过Interface还可以转换回来

反射的法则

上面就是一些反射的基本用法,常用的就是获取一个对象在运行中的一个状态,或者是针对运行中的一个不确定的对象进行修改。下面要说的是反射的法则。 如果你英文够好,并且网络自由,可以看看golang官方的博客: https://blog.golang.org/laws-of-reflection 里面详细描述了反射的三个法则,如果你看不到,那就只能听我下面吹一吹了。

  • Reflection goes from interface value to reflection object.
  • Reflection goes from reflection object to interface value.
  • To modify a reflection object, the value must be settable.

这三个就是官方给出的法则,我分别用自己的话解释一下。

反射就是将任意值转换为反射对象

在golang中我们知道interface就和java中的Object类似(只是类似而已),代表了所有类型,reflect包正是帮我们将任意的一个类型转换成了我们上面例子中看到的一个v,这个v就是反射对象。 通过这个反射对象中的一些方法我们才能看见原来的对象是什么样子。

反射对象可以转换为任意对象

这个正好与第一个相反,就像最后一个例子中给出的,反射获得的反射对象可以通过Interface方法转换为原来的对象。

如果你要修改反射对象,那么这个对象必须要可以被修改

什么意思呢?就如同这个案例中

代码语言:javascript
复制
func main() {
    a := 1.3
    v := reflect.ValueOf(&a)
    elem := v.Elem()
    elem.SetFloat(0.2)
    fmt.Println(a)
}

如果我们传递的并非a的地址并且直接使用v.SetFloat那么就会报错,因为我们无法对其进行修改,反射会帮我们copy一个,所以无法修改,只有当我们使用指针的时候才能修改。

同样的,和案例中的结构体操作一样,如果一个结构体的变量是小写的而不是大写的,证明外界没有办法访问到,所以也没有办法修改,也会出现异常。

反射的原理

下面就需要看看在源码中,反射到底是怎么实现的了。 我们着重看两个方法TypeOfValueOf

TypeOf

代码语言:javascript
复制
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	return toType(eface.typ)
}

我们先来看这个简单的TypeOf看到源码中很简单,通过unsafe.Pointer获得指针转换成emptyInterface类型

代码语言:javascript
复制
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}

然后通过toType方法得到具体类型

代码语言:javascript
复制
func toType(t *rtype) Type {
	if t == nil {
		return nil
	}
	return t
}

其中Type就包含了所有的信息,然后返回出去就完成了。

ValueOf

代码语言:javascript
复制
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {
	if i == nil {
		return Value{}
	}

	// TODO: Maybe allow contents of a Value to live on the stack.
	// For now we make the contents always escape to the heap. It
	// makes life easier in a few places (see chanrecv/mapassign
	// comment below).
	escapes(i)

	return unpackEface(i)
}

上面nil就不说了,主要方法是下面unpackEface

代码语言:javascript
复制
// 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}
}

我们可以看到其实与TypeOf一样,只不过多封装了一层Value,其中的word就是当前对象的指针,因为我们知道通过TypeOf得到的Value可以用很多操作。

代码语言:javascript
复制
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}

反射的意义

说了这么多,那么反射存在的意义到底在哪?说白了,我们在写代码的时候什么时候能用上它? 还是举个例子你就明白了。

json.Marshal案例

json.Marshal这个方法用过吧,是将任意对象转换成json,这个案例就足以说明反射的厉害了。 我们先自己想一下,如果要将一个对象转换成json:

  • 我们运行之前其实是不知道传入对象的类型,而且传入的对象不同,那么解析方式肯定不同,如果传入的是map或者传入的是struct肯定解析方式不同,所以方法内部需要动态的判断传入类型从而做操作。
  • 还有我们不知道传入的struct内部的属性长什么样子。

这个时候反射就能解决这样的问题,通过反射可以知道传入对象的类型,根据不同的类型做操作,同时可以获取到如struct这样类型内部的字段属性和值分别是多少。

json.Marshal源码分析

因为所有源码太多,我给出查看路线,然后给出上面所述的两处重点。 json.Marshal -> e.marshal -> e.reflectValue -> valueEncoder -> typeEncoder -> newTypeEncoder -> newStructEncoder -> se.encode

要点1 - newTypeEncoder
代码语言:javascript
复制
// newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
	...........

	switch t.Kind() {
	case reflect.Bool:
		return boolEncoder
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return intEncoder
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return uintEncoder
	case reflect.Float32:
		return float32Encoder
	case reflect.Float64:
		return float64Encoder
	case reflect.String:
		return stringEncoder
	case reflect.Interface:
		return interfaceEncoder
	case reflect.Struct:
		return newStructEncoder(t)
	case reflect.Map:
		return newMapEncoder(t)
	case reflect.Slice:
		return newSliceEncoder(t)
	case reflect.Array:
		return newArrayEncoder(t)
	case reflect.Ptr:
		return newPtrEncoder(t)
	default:
		return unsupportedTypeEncoder
	}
}

通过反射获得传入对象的类型,判断选择具体的编码器进行编码,如果传入的是map那就…如果传入的是struct那就…

要点2 - se.encode
代码语言:javascript
复制
func (se *structEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
	e.WriteByte('{')
	first := true
	for i, f := range se.fields {
		fv := fieldByIndex(v, f.index)
		if !fv.IsValid() || f.omitEmpty && isEmptyValue(fv) {
			continue
		}
		if first {
			first = false
		} else {
			e.WriteByte(',')
		}
		e.string(f.name, opts.escapeHTML)
		e.WriteByte(':')
		opts.quoted = f.quoted
		se.fieldEncs[i](e, fv, opts)
	}
	e.WriteByte('}')
}
代码语言:javascript
复制
func fieldByIndex(v reflect.Value, index []int) reflect.Value {
	for _, i := range index {
		if v.Kind() == reflect.Ptr {
			if v.IsNil() {
				return reflect.Value{}
			}
			v = v.Elem()
		}
		v = v.Field(i)
	}
	return v
}

encode这个方法是解析结构体的,我们可以清楚的看的从结构体中通过v.Field将里面的参数拿出来。 其他细节这里就不做说明了,主要的目的是要表示反射在其中起到的重要作用。

总结和提醒

看完你就应该清楚反射到底是做什么用的,具体我们什么时候会用到它。最后还要提醒一下,反射也存在两个必然的问题:

  • 第一个是不安全,因为反射的类型在转换中极易出现问题,所以使用需谨慎。
  • 第二个是速度慢,之所以有人抨击golang的json解析库慢,一部分原因就是因为其中涉及到了反射,所以如果对效率有要求的地方就要斟酌使用了。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-06-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 反射的定义
  • 反射的使用
    • reflect.TypeOf
      • reflect.ValueOf
        • Elem()
          • Field()
            • Interface()
            • 反射的法则
              • 反射就是将任意值转换为反射对象
                • 反射对象可以转换为任意对象
                  • 如果你要修改反射对象,那么这个对象必须要可以被修改
                  • 反射的原理
                    • TypeOf
                      • ValueOf
                      • 反射的意义
                        • json.Marshal案例
                          • json.Marshal源码分析
                            • 要点1 - newTypeEncoder
                            • 要点2 - se.encode
                        • 总结和提醒
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档