Go语言大厂编程 reflect 反射操作

530 阅读6分钟

计算中的反射是程序检查自身结构的能力,尤其是通过类型;这是元编程的一种形式。这也是一个很大的困惑来源。你可以先复习一下 Interface接口设计

类型和接口

我们先复习一下Go语言中的类型,因为反射机制是构建在类型系统之上的。

Go 语言中,每个变量都有一个静态类型,在编译阶段就确定了的,比如 int, float32, *MyType, []byte 等等。注意,这个类型是声明时候的类型,不是底层数据类型。

type MyInt int

var i int
var j MyInt

i的类型是int,j的类型是MyInt,但是它们有相同的底层类型int,如果不进行转换,它们就不能相互赋值。

类型的一个重要类别是接口类型,它表示固定的方法集。接口变量可以存储任何具体(非接口)值,只要该值实现接口的方法。一对著名的例子是io.Reader && io.Writer

// Reader is the interface that wraps the basic Read method.
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
    Write(p []byte) (n int, err error)
}

任何实现了这些方法的类型被称为实现了 io.Reader或者io.Writer接口

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

无论r的值是多少,他的静态类型总是io.Reader

另一个重要的例子,空接口:

interface{}

它可以理解成任意类型

any

它表示方法的空集合,任何值都可以满足它,因为每个值都有零个或多个方法。

interface并不是动态类型,它们是静态类型的:接口类型的变量始终具有相同的静态类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也始终满足接口的要求。

我们需要对接口有比较深刻的理解,因为反射和接口是密切相关的。

反射的定律

1.接口值到反射对象

反射只是一种检查接口变量中存储的类型和值对的机制,我们需要了解包反射中的两种类型:reflect.Typereflect.Value 。这两种类型允许访问接口变量的内容,reflect 包中提供了两个函数 reflect.TypeOf && reflect.ValueOf 来获取接口变量类型和值。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

输出:

type: float64

您可能想知道接口在哪里,因为程序看起来是在传递 float64变量x,而不是接口值来反映。但是我们可以从reflect.TypeOf的函数签名来看,它包含一个空接口:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

当我们调用reflect.TypeOf,首先会将x转换成interface空接口类型。之后解压该接口以恢复类型信息。

同理,我们可以尝试使用reflect.ValueOf

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

输出:

value: <float64 Value>

reflect.Value有一个返回反射reflect.Type方法。TypeValue都有一个Kind方法,该方法返回一个常量,表示存储的是哪种底层类型:Uint、Float64、Slice等等。还有一些名为IntFloat的方法可以获取存储在其中的值:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

输出:

type: float64
kind is float64: true
value: 3.4

2.反射对象到接口值

和物理反射一样,Go中的反射也会产生自己的逆反射。我们可以使用接口方法 Interface 恢复接口值;实际上,该方法将类型和值信息打包回接口表示,并返回结果:

// Interface returns v's current value as an interface{}.
// It is equivalent to:
//	var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
	return valueInterface(v, true)
}

我们可以这么做:

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

打印由反射对象v表示的float64值: 反射从接口值到反射对象,然后再返回

3.要修改反射对象,该值必须是可设置的。

第三定律是最微妙、最令人困惑的,但如果我们从第一原理出发,就很容易理解。

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(7.1) // Error: will panic.

输出:

settability of v: false
panic: reflect.Value.SetFloat using unaddressable value

可设置性有点像可寻址性,但更严格。反射对象可以修改用于创建反射对象的实际存储。可设置性由反射对象是否保留原始项决定:

var x float64 = 3.4
v := reflect.ValueOf(x)

我们应该理解在Go语言中,参数的传递是进行了值拷贝。因此我们无法对原x进行重新设置。在代码层面看来,x 其实是从reflect.ValueOf函数里的代码创建的,所以使用panic来杜绝可能引起的混乱。

如果我们期望改变数据本身,我们必须向函数传递x的指针(指向x的指针)

var x float64 = 3.4
p := reflect.ValueOf(&x)
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
//
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(7.1)
fmt.Println(x)

输出:

type of p: *float64
settability of p: false

settability of v: true
7.1

反射对象p是不可设置的,它本质上是*p,是一个地址值。我们真正需要操作的是p指向的内存空间。所以这里我们用p.Elem来获取。反射值需要某个对象的地址,以便修改它们所代表的内容

结构体修改

只要我们有这个结构的地址,我们就可以修改它字段的值。

下面是一个分析结构值t的简单示例,我们使用结构的地址创建反射对象,因为我们希望通过反射对象提取字段的名称和值,且稍后对其进行修改。

type T struct {
	Name string `json:"name"`
	age  int    `json:"age"`
}

t := T{"skidoo", 23}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
  f := s.Field(i)
  if f.CanInterface() {
    fmt.Printf("%d: %s %s = %v\n", i,typeOfT.Field(i).Name, f.Type(), f.Interface())
  }
}

输出:

0: A int = 23

我们可以看到只有大写的字段才能从反射对象中获取值,同理我们再设置值得时候也如此。

s.Field(0).SetString("Sunset Strip")
// s.Field(1).SetInt(77) //panic
fmt.Println("t is now", t)

输出:

t is now {23 Sunset Strip}

如果我们修改程序,使s是从t而不是&t创建的,那么对SetString的调用将失败,因为t的字段将不可设置。

结论

一旦你理解了这些规律,Go中的反射就会变得更容易使用,尽管它仍然很微妙。这是一个强大的工具,应该小心使用,除非绝对必要,否则应避免使用。

还有很多关于反射的内容我们还没有涉及到——在通道上发送和接收、分配内存、使用切片和映射、调用方法和函数——但这篇文章已经足够长了。我们将在后面的文章中介绍其中一些主题。