Golang 接口与反射知识要点

这篇文章以 Go 官方经典博客 The Laws of Reflection 为基础,详细介绍文中涉及的知识点,并有所扩展。

1. 接口类型变量

首先,我们谈谈接口类型的内存布局(memory layout),其他基础类型、Struct、Slice、Map、指针类型的内存布局会在以后单独分析。接口变量的值包含两部分内容:赋值给接口类型变量的实际值(concrete value),实际值的类型信息(type descriptor)。两部分在一起构成接口的值(interface value)。

接口变量的这两部分内容由两个字来存储(假设是 32 位系统,那么一个字就是 32 位),第一个字指向 itable (interface table)。itable 表示 interface 和实际类型的转换信息。itable 开头是一个存储了变量实际类型的描述信息,接着是一个由函数指针组成的列表。注意 itable 中的函数和接口类型相对应,而不是和动态类型。例如下面例子,itable 只关联了 Stringer 中定义的 String 方法,而 Binary 中定义的 Get 方法则不在其中。对于每个 interface 和实际类型,只要在代码中存在引用关系, go 就会在运行时为这一对具体的 <Interface, Type> 生成 itable 信息。

第二个字称为 data,指向实际的数据。例子中,赋值语句 var s Stringer = b 实际上对b做了拷贝,而不是对b进行引用。存放在接口变量中的数据大小可能任意,但接口只提供了一个字来专门存储真实数据,所以赋值语句在堆上分配了一块内存,并将该字设置为对这块内存的引用

type Stringer interface {
    String() string
}

type Binary uint64

func (i Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}

b := Binary(200)
var s Stringer = b

Go 是静态类型语言(statically typed)。一个接口类型的不同变量总是有同样静态类型,尽管在运行时,接口变量的保存的实际值会改变。下面例子中,无论 r 被赋予的什么实际值,r 的类型总是 io.Reader。

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

2. 类型断言

类型断言是一个使用在接口变量上的操作。

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

在这个例子中,r 被赋予了 tty 的一个拷贝,所以实际值是 tty。而实际类型是 os.File。需要注意到,os.File 类型自身还实现了除接口方法 Read 以外的方法。尽管接口变量只能访问 Read 方法,但接口的 data 字部分里携带了实际值的全部信息。因此我们可以有如下操作:

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

该赋值语句后边是一个类型断言。它断言的是 r 变量携带的元素,同时是 io.Writer 接口的实现,所以我们才能把 r 赋值给 w。赋值后的 w 可以访问 Write 方法,但无法访问 Read 方法了。

3. 鸭子类型

鸭子类型(duck typing)是动态类型和某些静态语言用到的一种对象推断风格。一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。这个概念也可以表述为:

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

鸭子类型像多态一样工作,但是没有继承。在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。在常规类型中,我们能否在一个特定场景中使用某个对象取决于这个对象的类型,而在鸭子类型中,则取决于这个对象是否具有某种属性或者方法 —— 即只要具备特定的属性或方法,能通过鸭子类型测试,就可以使用。鸭子类型的缺点是没有任何静态检查,如类型检查、属性检查、方法签名检查等。

Go 语言虽然是静态语言,但在接口类型中使用了鸭子类型。不同于其他鸭子类型语言的是,它实现了在编译时进行静态检查,比如变量是否实现接口方法、调用接口方法时参数个数是否相符,同时也不失鸭子类型带来的灵活和自由。

4. 反射机制

  • 什么是反射机制?

在计算机科学中,反射是指计算机程序在运行时(Runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

简单来说,反射只是一种机制,在程序运行时获得对象类型信息和内存结构。通常高级语言借助反射机制来解决,编译时无法知道变量具体类型,而只有等到运行时才能检查值和类型的问题。不同语言的反射模型不尽相同,有些语言还不支持反射。对于低级语言,比如汇编语言,由于自身可以直接和内存打交道,所以无需反射机制。

  • 使用反射的场景?

Go 语言中使用反射的场景:有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定,但事先无法不知道接受到的参数是什么类型,全部以 interface{} 类型接受。这时就需要对函数的参数进行反射,在运行期间动态地执行函数。感兴趣的读者可以参考 fmt.Sprint(a ...interface{}) 方法的源码。

5. reflect 包

TypeOf()、ValueOf()

reflect 包封装了很多简单的方法(reflect.TypeOf 和 reflect.ValueOf)来动态获得类型信息和实际值(reflect.Type,reflect.Value)。

var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))  // 打印 type: float64

var r io.Reader = strings.NewReader("Hello")
fmt.Println("type:", reflect.TypeOf(r))  // 打印 type: *strings.Reader

reflect.TypeOf 方法的函数签名是 func TypeOf(i interface{}) Type 。它接受任意类型的变量。当我们调用 reflect.TypeOf(x) 时,x 首先存储在一个空接口类型中,作为传参。reflect.TypeOf 解析空接口,恢复 x 的类型信息。而 reflect.ValueOf 可以恢复 x 实际值。

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

Type()、Kind()

reflect.Type 和 reflect.Value 都提供了很多方法支持来操作他们。1. reflect.Value 的 Type() 方法返回对应的 reflect.Type;2. reflect.Type 和 reflect.Value 都有 Kind() 方法,来获得实际值的类型对应 reflect 包中的常量;3. reflect.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())

打印结果:

shell script type: float64 kind is float64: true value: 3.4

有一点需要注意的是,Kind() 方法返回的是反射对象的底层类型,而不是静态类型。比如,如果反射对象接受一个用户定义的整数型变量:

func main() {
    type MyInt int
    var x MyInt = 7
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())
    fmt.Println("kind is int:", v.Kind() == reflect.Int)
    fmt.Println("value:", v.Int())
}

打印结果:
shell script type: main.MyInt kind is int: true value: 7

v 调用 Kind() 仍是 reflect.Int,即使 x 的静态类型是 MyInt 而不是 int。总而言之,Kind() 方法无法区分来自 MyInt 的整型,但 Type() 方法可以

Interface()

Interface() 方法能从 reflect.Value 变量中恢复接口值,是 ValueOf() 的逆向。注意的是,Interface() 方法返回总是静态类型 interface{}。

6. 反射对象的可设置性

SetXXX(), CanSet()

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

运行上面的例子,我们可以发现 v 不可修改(settable)。可设置性(Settability)是 reflect.Value 的一个特性,但不是所有的 Value 都有。

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

Elem()

由于是 x 的一个拷贝传入 reflect.ValueOf,所以 reflect.ValueOf 创建的接口值也是 x 的一个拷贝,不是原 x 本身。因此修改反射对象,无法修改 x,反射对象不具有可设置性。

显然,要使反射对象具有可设置性。传入 reflect.ValueOf 的参数应该是 x 的地址,即 &x。

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

反射对象 p 仍是不可设置的,因为我们不是要设置 p,而是 p 所指向的内容。使用 Elem 方法获取。

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())  // settability of v: true

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

7. Struct 的反射

NumField(), Type.Field()

我们用 struct 的地址来创建反射对象,这样后续我们可以修改这个 struct:

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\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}

打印结果:

0: A int = 23
1: B string = skidoo

Value.Field()

T 的字段必须是首字母大写的才可以设置,因为只有暴露的 struct 字段,才具有可设置性

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

参考文件

The Laws of Reflection

Go Data Structures: Interfaces

Go 语言的数据结构:Interfaces

浅析 Golang Interface 实现原理

深度解密Go语言之反射

Golang 接口与反射知识要点的更多相关文章

  1. golang:reflect反射

    因为之前一直以C++为主要开发语言,所以刚接触go语言中的reflect时感觉很懵逼,因此决定找资料彻底学习一下. 到底反射是什么? https://blog.golang.org/laws-of-r ...

  2. JAVA 构造器, extends[继承], implements[实现], Interface[接口], reflect[反射], clone[克隆], final, static, abstrac

    记录一下: 构造器[构造函数]: 在java中如果用户编写类的时候没有提供构造函数,那么编译器会自动提供一个默认构造函数.它会把所有的实例字段设置为默认值:所有的数字变量初始化为0;所有的布尔变量设置 ...

  3. [CISCO] 转载:冲突域与广播域(区别、知识要点)

    [CISCO] 转载:冲突域与广播域(区别.知识要点) 1.传统以太网操作(Ethernet Connection Ethernet) 传统共享式以太网的典型代表是总线型以太网.在这种类型的以太网中, ...

  4. Go语言的接口与反射

    美女图片没啥用,就是为了好看 本文还在完善中... go总体而言是一门比较好入门的语言,许多特性都很精简易懂,但是接口与反射除外.他们真的让人头疼,不知道是自身资质问题还是怎么着,总是觉得很多书上写的 ...

  5. RIP 知识要点

    RIP知识要点: UDP:520 版本:v1(广播包更新) / v2(组播更新 224.0.0.9  ) 度量值:跳数(最多跳15跳,路由为16跳时路由不可达) =================== ...

  6. 总结了零基础学习Java编程语言的几个基础知识要点

    很多Java编程初学者在刚接触Java语言程序的时候,不知道该学习掌握哪些必要的基础知识.本文总结了零基础学习Java编程语言的几个基础知识要点. 1先了解什么是Java的四个方面   初学者先弄清这 ...

  7. Golang通脉之反射

    什么是反射 官方关于反射定义: Reflection in computing is the ability of a program to examine its own structure, pa ...

  8. golang中的反射reflect详解

    先重复一遍反射三定律: 1.反射可以将"接口类型变量"转换为"反射类型对象". 2.反射可以将"反射类型对象"转换为"接口类型变量 ...

  9. CentOs7下systemd管理知识要点

    centOs7的一个巨大的变动就是用systemd取代了原来的System V init.systemd是一个完整的软件包,安装完成后有很多物理文件组成,大致分布为,配置文件位于/etc/system ...

随机推荐

  1. ZYNQ Block Design中总线位宽的截取与合并操作

    前言 在某些需求下,数据的位宽后级模块可能不需要原始位宽宽度,需要截位,而某些需求下,需要进行多个数据的合并操作. 在verilog下,截位操作可如下所示: wire [7:0] w_in; wire ...

  2. 网页播放摄像头视频一种新的实现方式(非ocx方式)

    前言 出于安全性考虑,浏览器对网页调用本地资源做了诸多限制.单纯的js是不能调用本地摄像头的,最常用的解决方案是通过ocx来实现.ocx是IE浏览器的扩展插件,并不是通用标准,很多浏览器并不支持ocx ...

  3. 2019DX#10

    Solved Pro.ID Title Ratio(Accepted / Submitted)   1001 Minimum Spanning Trees 22.22%(2/9)   1002 Lin ...

  4. 2019DX#3

    Solved Pro.ID Title Ratio(Accepted / Submitted)   1001 Azshara's deep sea 凸包 6.67%(6/90)  

  5. codeforces 816 C. Karen and Game(模拟+思维)

    题目链接:http://codeforces.com/contest/816/problem/C 题意:给出一个矩阵,问能否从都是0的情况下只将一整行+1或者一整列+1变形过来,如果可以输出需要步数最 ...

  6. HTML5 01. 布局、语义化标签、智能化表单、表单元素/标签/属性/事件、多媒体、类操作、自定义属性

    1.知识点 lang = “en”   所用语言是英文 文档结构更简洁 IE8一下不支持h5c3 书写更宽松 div没有语义 标签语义化:在合适的地方使用合适的标签 对seo优化友谊 网页经典布局 页 ...

  7. 【Offer】[58-2] 【左旋转字符串】

    题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部.请定义一个函数实现字符串左旋转操作的功能.比如,输入字符串"a ...

  8. 题解 洛谷P5259【欧稳欧再次学车】

    实际上没什么可说的,暴力大模拟就好. 一定要开long long! 一定要开long long! 一定要开long long! (不然会炸数据的!!!) //Stand up for the fait ...

  9. Python作业本——第4章 列表

    课后习题: 1.[]是一个空列表 2. spam.insert(3, 'hello') 错,应为:spam[2] = 'hello' 3.['d']    'd' 4.['d']    'd' 5.[ ...

  10. Springboot源码分析之TypeFilter魔力

    摘要: 在平常的开发中,不知道大家有没有想过这样一个问题,为什么我们自定义注解的时候要使用spring的原生注解(这里指的是类似@Component,@Service........),要么就是 随便 ...