Golang 接口与反射知识要点

目录

  • Golang 接口与反射知识要点

    • 1. 接口类型变量
    • 2. 类型断言
    • 3. 鸭子类型
    • 4. 反射机制
    • 5. reflect 包
      • TypeOf()、ValueOf()
      • Type()、Kind()
      • Interface()
    • 6. 反射对象的可设置性
      • SetXXX(), CanSet()
      • Elem()
    • 7. Struct 的反射
      • NumField(), Type.Field(i int)
      • Value.Field(i int)
    • 参考文档

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. 反射机制

  • 什么是反射机制?

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

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

  • 使用反射的场景?

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() 方法返回实际类型信息;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 的整数型和 int 型,但 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(i int)

我们用 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())
}

Type.Field(i int) 方法返回字段信息,一个 StructField 类型的对象,包含字段名等。

打印结果:

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

Value.Field(i int)

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}

Value.Field(i int) 返回 struct s 的字段实际值,所以可以用来设置操作。注意 Type.Field(i int) 和 Value.Field(i int) 的用途区别:前者总是负责和实际类型信息获取相关的操作,后者是与实际值相关的操作

参考文档

The Laws of Reflection

Go Data Structures: Interfaces

Go 语言的数据结构:Interfaces

浅析 Golang Interface 实现原理

深度解密Go语言之反射

原文地址:https://www.cnblogs.com/guangze/p/11621277.html

时间: 2024-10-09 05:14:49

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

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

很多Java编程初学者在刚接触Java语言程序的时候,不知道该学习掌握哪些必要的基础知识.本文总结了零基础学习Java编程语言的几个基础知识要点. 1先了解什么是Java的四个方面   初学者先弄清这些Java的基本概念也是必不可少的,死记硬背肯定是不行的,重在理解,理解它们之间的区别与联系,分别有哪些应用.想想这些代码中用到了哪些知识点.不要一味地照着书本敲代码.而不去理解. 1.Java编程语言,即语法. 2.Java文件格式,即各种文件夹.文件的后缀. 3.Java虚拟机(JVM),即处理

javascript必须知道的知识要点(二)

该文章不详细叙述各知识要点的具体内容,仅把要点列出来,供大家学习的时候参照,或者检测自己是否熟练掌握了javascript,清楚各个部分的内容. 内建对象可划分为数据封装类对象.工具类对象.错误类对象,如下. 数据封装类对象: Number对象 String对象 Boolean对象 Array对象 Object Function 工具类对象: Math对象 Date对象 RegExp对象 错误类对象: Error对象  Number对象 JavaScript 只有一种数字类型.Number,包括

CentOs7下systemd管理知识要点

centOs7的一个巨大的变动就是用systemd取代了原来的System V init.systemd是一个完整的软件包,安装完成后有很多物理文件组成,大致分布为,配置文件位于/etc/systemd这个目录下,配置工具命令位于/bin,和/sbin这两个目录下,预先准备的备用配置文件位于/lib/systemd目录下,还有库文件和帮助手册等等.这是一个庞大的软件包.详情使用rpm -ql systemd即可查看. systemd已经不仅仅是一个启动管理软件,而且是一个综合性的服务管理软件,它

三、Java基础知识要点合集

1.数据类型 Java的数据类型分为基本数据类型和引用数据类型. a, 基本数据类型包括:byte, boolean, char, short, int, long, float, double; b, 每个基本数据类型都有相应的引用类型,比如int  ->  Integer,char -> character. c, 查询不同类型数据的范围,方法之一是可以用基本类型对应的引用类型.比如,"int i = Integer.Size; "(i表示int型数据所占的位(bit)

【转】MongoDB 知识要点一览

原文链接 http://www.cnblogs.com/zhangzili/p/4975080.html MongoDB 知识要点一览 1.启动mongoDb数据库: 进入mongoDB的安装目录,执行如下命令 C:\Program Files\MongoDB\Server\3.0\bin>mongod.exe --dbpath "C:\Program Files\MongoDB\Server\3.0\db" 启动成功后在打开一个cmd窗口,进入mongoDB的安装目录,执行mo

Navicat for SQLite 表检查知识要点

Navicat for SQLite 作为一款比较常见的数据库管理软件,很多的人都会用到它,但是一些用户对其了解的很少,很多要点都不是很清楚.下面就给大家介绍介绍Navicat for SQLite 表检查知识要点,检查限制是最通用的限制类型,允许指定在某列的值必须符合一个 Boolean(真值)表达式. Navicat for SQLite 在“检查”选项卡,只需简单地点击检查栏位即可进行编辑.使用检查工具栏,用户可以创建新的.编辑或删除选择的检查栏位.需要注意的是,只有 SQLite 3.3

java web的开发 知识要点

近期闲下来时写的一个有关 java web的开发 的 常用架构 的总结,用于初 学 者或团队新人培训. Java开发初步.ppt SSH  为 struts+spring+hibernate 的一个集成框架,是目前较流行的一种JAVA Web应用程序开源框架 SSI   为 Strtus2.Spring.iBatis java web的开发 知识要点

golang接口的使用(练习一)

在go语言中,一个类只要实现了接口要求的所有函数,我们就说这个类实现了这个接口.golang接口赋值实现方式一:将对象实例赋值给接口 package main import "fmt" //定义一个Animal接口,实现飞和跑的功能 type Animal interface { Fly() Run() } //定义一个Bird类 type Bird struct { } //通过类实现接口的函数功能 func (bird Bird) Fly() { fmt.Println("

Python原来这么好学-1.3节: 知识要点总结与内容复习

这是一本教同学们彻底学通Python的高质量学习教程,认真地学习每一章节的内容,每天只需学好一节,帮助你成为一名卓越的Python程序员: 本教程面向的是零编程基础的同学,非科班人士,以及有一定编程水平的中高级程序员. ? 本章总结 在本章的内容中,主要讲解了如何在Windows以及Linux系统中安装python. 现在分别对本章中介绍过的命令工具,以及知识要点进行复习. 实用工具 知识要点 1) 环境变量保存了系统运行环境的一系列参数,比如系统环境变量PATH,其保存了与路径相关的参数. 系