Go中的反射

Go语言中的反射

翻译自官方英文文档
https://go.dev/blog/laws-of-reflection

介绍

一个程序中反射的运算,是程序对自身结构进行考察的一种能力,尤其是通过类型,
反射是一种元编程的形式,同样也是编程中一大困惑之源

本文中, 我们努力阐述清楚反射在go语言中的工作原理, 每一种语言的反射模型
都不一样, 有的语言则根本不支持, 本文针对go语言, 所以后文中的反射一词
都应该被理解为 “go中的反射”

本博文写于 2021 年, 早于泛型的加入, 虽然文章中的重点并没有因为语言的发
展而 变成错误内容, 但还是稍加修改以避免让熟悉go新特性的人感到困惑

类型与接口

因为反射是建立在类型系统上的, 所以首先来复习一下go的类型系统
Go语言是静态类型的, 每一个变量都有一个静态类型, 也就是说在编译时某个类型
是完全已知且固定的:int, float32, *MyType, []byte 等等.如果我们如此声明

type MyInt int
var i int
var j myInt

i具有类型int, j具有类型MyInt, 变量i和j具有不同静态类型,并且,虽然他们具
有相同的底层类型, 但他们不能不经转换就相互赋值

一种重要的类型是接口类型, 接口类型代表了一组固定的方法, 一个接口变量可以
存储任何实现了接口中的方法的具体值, 一个大家熟知的例子是io.Reader和
io.Writer Reader和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)
}

任何实现了一个Read或者Write方法的类型被称为实现了 io.Reader 或者 io.Writer

这段讨论的目的是想表明一个类型为io.Reader的变量,可以持有任何具有Read方法的值

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)

需要明确的是不管r持有的是何种具体的值, r的类型始终是 io.Reader, Go 是静态类型
的而 r 的静态类型是 io.Reader

一个重要的例子是一个空的 interface

interface{}

或者它的等价别名

any

它代表了一个空的接口, 任何值都满足这个接口, 因为任何一个值要么没有方法, 要么有多个
个方法.

一些人说 go 的 interface 是一种动态类型, 但这么说是有误导性的.interface是静态类型的,
interface类型的变量总是具有相同的静态类型, 即时在运行时存储在 interface 变量中的值类
型会发生变化, 这个值总会满足 interface

我们之所以明确这些, 是因为反射和接口密切相关

The representation of an interface

一个接口类型存储了一对数据, 赋值给接口变量的 “具体值” 和这个 “值类型” 的描述,更
明确的说, 值是实现了接口的具体的底层数据项, 类型则是该项的完整的类型描述,例如

var r.io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

r包含了(值,类型)对,(tty,*os.File)
要注意的事 *os.File 实现了除了Read以外的其他方法, 即使 interface 只提供了对 Read 方
法的访问, interface 内部携带了关于这个值的全部类型信息, 这也是为什么我们可以做以下操作

var w io.Writer
w = r.(io.Writer)

这是一个断言语句 , 因为 r 同样实现了 io.Writer
interface 的静态类型决定了该接口中哪些方法可以被调用,而具体值可能实现了一个更大的方法
集合

继续看一个例子

var empty interface{}
empty = w

空的 interface 同样会携带上面所说的数据对,其内部同样包含完整的类型信息
一个重要的细节是,存储在 interface 中的数据对形式只会是(值,具体类型),而不可能是(值,接口类型)
接口不能持有接口值

现在我们可以学习反射了

反射的第一条规则

1.反射将interface值变为反射对象

从基本原理来看,反射是一种考察储存在 interface 变量内部的值-类型对的机制, 首先我们需要知道
反射包中提供了两个类型, Type 和 Value, 这两个类型提供了 interface 变量的内容的访问入口,两个
简单 方法 reflect.TypeOf 和 reflect.ValueOf, 通过这两个方法可以检索出接口值中的 reflect.Type
和 reflect.Value (此外,从 reflect.Value 中也可以很容易的获取到对应的 reflect.Type, 但这里
还是让类型和值的概念尽量独立开来)

示例

package main

import (
    "fmt"
    "reflect"
)

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

程序输出

type: float64

你可能会困惑于在这个例子中 interface 在哪里, 因为在这个例子中似乎是直接把一个 float64
变量x作为传入参数而不是一个 interface 值, 其实 interface 出现在这里, reflect.TypeOf
的 参数是一个空的 interface

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

当我们调用 reflect.TypeOf(x) x 首先被存储在一个空的 interface 中,然后被作为参数传入,
reflect.TypeOf 解析空 interface 复原出类型信息

同样, reflect.ValueOf 方法则复原值的信息

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

输出

value: <float64 Value>

这里我们显式的调用 String 方法, 是应为 fmt 包会去深挖 reflect.Value 并显示出其内部的
具体的值(如果不调用 .String() 打印结果会是 3.4 String 方法则会打印出值类型信息的字符串)

reflect.Type 和 reflect.Value 都有大量大量的方法, 可以让我们对其进行各种操作. 其中一
个重要的方法是 Value 具有一个 Type 方法, 其返回 reflect.Value 的 Type
(等价于reflect.Type), 另一个特点是 Type 和 Value 都具有一个 Kind 方法, 其返回一个常
量值指出其中存储了哪种数据: Uint, Float64, Slice 等等, 此外 Value 方法具有一些名为 Int,
Float 方法, 可以让我们提取出其内部存储的实际值

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

此外还有一些方法形如 SetInt 和 SetFloat 使用他们我们需要了解 “可设置性”, 反射的第三条
规则中我们会进行讨论

反射库有两个特点值得挑出来一讲, 第一点是保持 API 的简洁, getter 和 setter 方法, 使用最大
的值类型: 所有的有符号整形都用 int64. 想要得到实际的类型, 需要进行转换

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())                            // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())       

第二个特点是反射对象的Kind方法描述的事底层基础类型, 而不是静态类型, 如果一个反射对象包含
一个用户自定义的整形类型, 如下

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

Kind 方法返回的仍是 reflect.Int, 虽然其静态类型是 MyInt, 而非 int, 换言之, Kind方法不
能区分MyInt和int, 但Type可以

反射的第二条规则

从反射对象变为 interface

就像物理上的反射, Go中的反射也可以产生其自身的逆反

对于一个已有的 reflect.Value 我们可以通过 Interface 方法复原出原来的interface, 实际上
这种方法是把类型和值的信息打包进一个interface并作为结果返回

// Interface return v`s value as an interface{}
func (v Value) Interface() interface{}

结果

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

打印出反射对象所代表的 float64 的值

还可以更进一步, 对于 fmt.Println, fmt.Printf 等方法传入的参数都是一个空的 interface
fmt 包内部会将 iterface 解包,就像我们之前的例子那样做的.

因此,只需要将 Interface 方法的结果传入给 fmt 打印的例程, 就可以正确输出 reflect.Value
的内容

fmt.Println(v.Interface())

由于fmt包内部的改变, 现在fmt包会自动解包 reflect.Value 如下, 可以直接将reflect.Value
传入

fmt.Println(v)

由于我们的值是一个 float64 我们还可以使用浮点数格式化输出

fmt.Printf("value is %7.le\n", v.Interface())

结果

3.4e+00

简而言之, Interface 方法是 ValueOf 的逆方法, 此外它返回的结果的静态类型总是interface{}

第三条规则

要改变反射对象,值必须是可设定的

这个特性很容易让人困惑, 但如果我们从第一条原则出发, 就很容易理解
这里有一段不能运行的代码, 但值得研究一下

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果运行这段代码, 他会报出异常信息

panic: reflect.Value.SetFloat using unaddressable value

问题并非是7.1这个值是不可寻址的, 而是v是不可设置的, 可设置性是反射值得一个属性, 而并不
是所有的反射值都具有可设置性

Value 的 CanSet 可以给出 Value 是否是可设置的,例如

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

输出结果

settability of v: false

对于一个不可设置的 Value 使用 Set 方法会导致错误,
但什么又是可设置性?

可设置性指的是反射对象能够去修改一开始用来创建反射对象的
实际的值, 是否可设置取决于反射对象持有的是不是原始的值

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

这段代码中, 传给 reflect.ValueOf(x) 的是x的一个拷贝,
所以在 reflect.ValueOf 的参数 interface 值创建是使用
的参数 x 的一个拷贝, 而非 x 变量本身, 如果下面的代码

v.SetFloat(7.1)

如果这个操作可以成功, 被改变的值会是 x 的拷贝的值, 而
并非 x 本身, 这个操作是没有意义的, 所以这种操作被定义非
法, 可设置性就是用来避免这个问题

如果我们想要改变 x 的值, 我们必须传递一个指针给反射库,
指针则指向我们想要修改的变量

如下

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

输出结果

type of p: *float64
settability of p: false

反射对象 p 的可设置性是 false, 但是我们要设置的并不是指针 p 本身,
(这里之所以是false, 是因为 p.CanSet() 获取的是指针 p 本身的
可设置性, 但我们并不是要改变指针的指向, 所以他的可设置性是false)

为了获取指针指向了谁, 我们需要使用 Elem 方法

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

输出结果

settability of v: true

由于 v 已经代表了 x 本身, 我们终于可以使用 v.SetFloat 来修改 x 的

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

结果

7.1
7.1
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值