golang 反射基本原理及用法

简介: golang 反射基本原理及用法

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站

类型和接口

Go 是静态类型语言。每一个变量都有一个静态的类型,即在编译时类型已知且固定:比如 intfloat32

接口类型

接口类型是类型的一个重要类别,它表示固定的方法集。接口变量可以存储任何具体值(非接口),只要该值实现接口的方法即可。如:

// Reader 是封装基本 Read 方法的接口
type Reader interface {
    Read(p []byte) (n int, err error)
}
// Writer 是封装基本 Write 方法的接口
type Writer interface {
    Write(p []byte) (n int, err error)
}

任何实现了 Read(p []byte) (n int, err error) 方法的类型都被称为实现了 Reader 接口(Writer 同理)。这意味着

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{}

它表示空的方法集,并且任何值都满足空接口,因为任何值都有零个或者多个方法。

有人说 Go 的接口是动态类型的,但这会产生误导。接口是静态类型的:接口类型的变量始终具有相同的类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也始终满足接口的要求。

接口的表示形式

**接口类型的变量存储了一对值:分配给该变量的具体值,以及该值的类型描述。**更确切地说,该值是实现接口的基础具体数据项,而类型描述了该数据项的完整类型。例如:

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

r 中包含了 (value, type) 对,即 (tty, *os.File)。请注意,类型 *os.File 实现的方法不只有 Read

尽管接口仅提供对 Read 方法的访问,但是其内部的值仍包含有关该值的所有类型信息。这就是为什么我们可以做下面的事情:

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

因为 r 的具体类型里面包含了 Write 方法,而 r 里面包含的值依然持有它原来的值,所以这个断言是没有问题的。

一个重要的细节是,接口内始终保存 (值, 具体类型) 形式的元素对,而不会有 (值, 接口类型) 的形式。接口内部不持有接口值。

反射

反射第一定律:从接口值反射出反射对象

反射对象主要有两类:reflect.Typereflect.Value

从底层讲,反射只是一种检查存储在接口变量中的值和类型对的机制。首先,我们需要了解反射包的两个类型:TypeValue

通过这两个类型可以访问接口变量的内容。还有两个函数 reflect.TypeOfreflect.ValueOf,它们可以从接口值中取出

reflect.Typereflect.Value。(另外,从 reflect.Value 可以很容易地获取到 reflect.Type,但是让我们暂时将 ValueType 的概念分开。)

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

上面的代码看起来像将 float64 类型的变量 x 传递给了 reflect.TypeOf,而不是传递的接口值。但实际上,传递的是接口;

// TypeOf 返回 interface{} 中值的反射类型
func TypeOf(i interface{}) Type

当我们调用 reflect.TypeOf(x) 时,x 先被存在一个空接口中,然后再作为参数传递;reflect.TypeOf 从该空接口中恢复类型信息。

相应的,reflect.ValueOf 函数会恢复值信息。

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

reflect.Typereflect.Value 都有许多方法可以让我们执行检查和操作:

  • Value 具有 Type 方法,该方法返回 reflect.ValueType 类型。
  • TypeValue 都有一个 Kind 方法,该方法返回 go 的类型(语言本身的类型,而不是自定义的类型)
  • Value 的很多方法,名字类似于 IntFloat64,可以让我们获取存储在里面的值。
  • 还有诸如 SetIntSetFloat 之类的方法,可以修改接口的值。

反射第二定律:从反射对象到接口值

给定 reflect.Value,我们可以使用 Interface() 方法恢复接口值;

实际上,该方法将类型和值信息打包回接口表示形式并返回结果:

//接口返回v的值作为接口{}。
func (v Value) Interface() interface{}

结果,我们可以说

y := v.Interface().(float64) // y的类型为float64
fmt.Println(y)

打印反射对象 v 表示的 float64 值。一种更简洁的写法是:

// fmt.Println 本身就接受 interface{} 参数
fmt.Println(y)

反射第三定律:要修改反射对象,该值必须可设置

不可设置的例子:

var x float64 = 3.4
v:= reflect.ValueOf(x)
// panic: reflect.Value.SetFloat using unaddressable value
v.SetFloat(7.1)//错误:会panic错误。

因为调用 reflect.ValueOf(x) 的时候,函数只拿到了 x 的副本,而不是 x 变量本身,如果我们在函数内部修改了 x 那也只是修改了副本而已。

ValueCanSet 方法报告 Value 的可设置性:

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

如果我们想修改它,可以在反射的时候,直接使用 x 的指针:

var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意:取 x 的地址
// type of p: *float64
// settability of p: false
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

我们注意到,这里我们使用了指针,但依然是不能设置其值。这是因为反射对象 p 是不可设置的,实际上我们想要设置的不是 p,而是 *p。为了获取 p 指向的内容,我们调用 Value 值的 Elem 方法,该方法指向指针:

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

现在,v 是一个可设置的反射对象了,我们可以使用 v.SetFloat 来修改 x 的值了:

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

反射值需要变量的地址才能修改其表示的值。

结构体

在下面的例子中,我们使用结构体的地址创建反射对象,因为稍后将要对其进行修改。然后我们将 typeOfT 设置为其反射类型,

并使用简单的方法调用对字段进行迭代。

type T struct {
    A int
    B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v.", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}
0: A int = 23
1: B string = skidoo

此处传递的内容还涉及可设置性的另一点:T 的字段名是大写(已导出),因为只能设置结构体的导出字段。

因为 s 包含可设置的反射对象,所以我们可以修改结构的字段:

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

如果我们修改代码从 t 而不是 &t 创建 s,则对 SeteIntSetString 的调用将失败,因为无法设置 t 的字段。

结论

反射定律:

  • 反射可以从接口值到反射对象
  • 反射可以从反射对象到接口值
  • 要修改反射对象,该值必须可设置。

参考文档


目录
相关文章
|
1月前
|
存储 安全 测试技术
GoLang协程Goroutiney原理与GMP模型详解
本文详细介绍了Go语言中的Goroutine及其背后的GMP模型。Goroutine是Go语言中的一种轻量级线程,由Go运行时管理,支持高效的并发编程。文章讲解了Goroutine的创建、调度、上下文切换和栈管理等核心机制,并通过示例代码展示了如何使用Goroutine。GMP模型(Goroutine、Processor、Machine)是Go运行时调度Goroutine的基础,通过合理的调度策略,实现了高并发和高性能的程序执行。
122 29
|
1月前
|
负载均衡 算法 Go
GoLang协程Goroutiney原理与GMP模型详解
【11月更文挑战第4天】Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理,创建和销毁开销小,适合高并发场景。其调度采用非抢占式和协作式多任务处理结合的方式。GMP 模型包括 G(Goroutine)、M(系统线程)和 P(逻辑处理器),通过工作窃取算法实现负载均衡,确保高效利用系统资源。
|
4月前
|
存储 关系型数据库 Go
SOLID原理:用Golang的例子来解释
SOLID原理:用Golang的例子来解释
|
4月前
|
算法 NoSQL 关系型数据库
熔断原理与实现Golang版
熔断原理与实现Golang版
|
7月前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
472 1
|
7月前
|
JSON 监控 安全
Golang深入浅出之-Go语言中的反射(reflect):原理与实战应用
【5月更文挑战第1天】Go语言的反射允许运行时检查和修改结构,主要通过`reflect`包的`Type`和`Value`实现。然而,滥用反射可能导致代码复杂和性能下降。要安全使用,应注意避免过度使用,始终进行类型检查,并尊重封装。反射的应用包括动态接口实现、JSON序列化和元编程。理解反射原理并谨慎使用是关键,应尽量保持代码静态类型。
103 2
|
7月前
|
JSON 编译器 Go
Golang深入浅出之-结构体标签(Tags):JSON序列化与反射应用
【4月更文挑战第22天】Go语言结构体标签用于添加元信息,常用于JSON序列化和ORM框架。本文聚焦JSON序列化和反射应用,讨论了如何使用`json`标签处理敏感字段、实现`omitempty`、自定义字段名和嵌套结构体。同时,通过反射访问标签信息,但应注意反射可能带来的性能问题。正确使用结构体标签能提升代码质量和安全性。
337 0
|
7月前
|
Java 编译器 Go
Golang底层原理剖析之内存逃逸
Golang底层原理剖析之内存逃逸
54 0
|
3月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
144 4
Golang语言之管道channel快速入门篇
|
3月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
72 4
Golang语言文件操作快速入门篇