面向对象
面向对象有三大特点:封装、继承和多态
1.方法
给内置类型定义方法是不被允许的
接收者
接口类型是无效的方法接收者
在之前的 学习笔记(二) 中有提到过方法,其格式如下:
func (接收者) func_name(参数) 返回值 {
//操作
}
学习了函数调用栈后,我们要知道 —— 接收者在函数调用中,其实是作为函数的参数来传递的。举个例子:
package main
import "fmt"
type A struct {
name string
}
func (a A) getname() string { //值传递,不修改原本变量
return a.name
}
func (pa *A) setname() { //指针传递,修改原本变量
pa.name = "Hi ! " + pa.name
}
func main() {
a := A{name: "Golang"}
pa := &a
//用方法变量来调用函数
fmt.Println(a.getname()) //查看汇编代码,发现调用形式是:A.getname(a)
a.setname() //语法糖,编译期间会为转为 (&a).setname(),即(*A).getname(pa)
fmt.Println(pa.getname()) //语法糖,编译期间会为转为 (*pa).setname(),即A.getname(a)
pa.setname() //调用形式是:(*A).getname(pa)
//用方法表达式来调用函数,不建议这种方法
//一、struct值只能调用 值接收者 的方法
fmt.Println(A.getname(a)) //与fmt.Println(A.getname(*pa))等价
A.setname(a) //会报错
//二、struct指针能调用 值接收者 和 指针接收者 的方法
(*A).setname(&a) //与(*A).getname(pa)等价
fmt.Println((*A).getname(&a)) //经过编译器处理,最后还是调用了 A.getname(a)
}
注意:上述语法糖是在编译期间发挥作用的,所以像 A{name: "hello"}.setname() 这种不能在编译期间拿到地址的字面量,就没办法通过语法糖转换,会报错。
(*A).setname(&A{name: "hello"}) 用方法表达式的方式调用就能正常执行。
下面列几个图,更直观的展现不同接收者的方法调用:
方法表达式
上面一直在提方法表达式,方法表达式到底是个什么东西?
答:方法表达式就是 Function Value。
方法变量
上图的 a.GetName 就是一个方法变量。
方法变量实际上就是个闭包,是捕获了方法接收者的 Function Value。
既然知道了方法变量是闭包,那就需要注意:闭包的捕获列表可能只进行值拷贝,也可能造成变量逃逸。这些在之前的 函数进阶 中有讨论过,这里就不在赘述。
2.接口
在了解接口前,不得不先理解 Go 语言的类型系统:
类型系统
在 Go 语言中,内置类型和自定义类型都有对应的类型描述信息,称为它的 “类型元数据”,每种类型元数据都是全局唯一的。这些类型元数共同构成了 Go 语言的类型系统。
作为类型元数据头部信息的 Type 的数据结构
源码文件:src/internal/abi/type.go line:20
type Type struct {
Size_ uintptr //类型大小
PtrBytes uintptr //含有所有指针类型前缀大小
Hash uint32 //类型hash值;避免在哈希表中计算
TFlag TFlag //二外类型信息标志
Align_ uint8 //该类型变量对齐方式
FieldAlign_ uint8 //该类型结构字段对齐方式
Kind_ uint8 //类型编号
Equal func(unsafe.Pointer, unsafe.Pointer) bool //用于比较该类型对象的函数
GCData *byte //gc数据
Str NameOff //类型名字的偏移
PtrToThis TypeOff //指向该类型的指针类型,可以为零
}
这只是作为头部信息,在 Type 之后还要存储各种类型额外需要描述的信息:
在之前的版本中,Type 结构体是写在 src/runtime/type.go 中的,且结构体名称为 _type;
现在则是写在 src/internal/abi/type.go 中,在 runtime/type.go 中,以 type _type = abi.Type 别名使用
如果是自定义类型,后面还会有一个 UncommonType 结构体:
源码文件:src/internal/abi/type.go line:197
type UncommonType struct {
PkgPath NameOff // 记录类型所在的包路径
Mcount uint16 // 记录该类型的方法数
Xcount uint16 // 记录该类型导出的方法数
Moff uint32 // 记录这些方法元数据组成的数组相对于 UncommonType 结构体的偏移 offset from this uncommontype to [mcount]Method
_ uint32 // unused
}
源码文件:src/internal/abi/type.go line:186
type Method struct {
Name NameOff // name of method
Mtyp TypeOff // method type (without receiver)
Ifn TextOff // fn used in interface call (one-word receiver)
Tfn TextOff // fn used for normal method call
}
方法元数据的数据结构
接口(非空)
在 Go 语言中,接口是一种类型。
接口的定义
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。
如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。
定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
接口的实现
package main
import "fmt"
type storage struct {
disk string
}
type io_operate interface {
read() *storage
write(str string) *storage
}
func (A *storage) read() *storage {
fmt.Println(A.disk)
return A
}
func (A *storage) write(str string) *storage { //storage 实现了 io_operate 定义的方法,称其实现了该接口
A.disk = str
return A
}
func main() {
var x io_operate //接口类型变量能够存储所有实现了该接口的实例
a := storage{disk: "A"}
x = &a //因为是指针接收者实现方法,所以要取地址
x.write("X")
a.read() //X
x.read() //X
a.write("B")
x.read() //B
a.read() //B
}
可以看到,通过接口类型变量调用方法 和 通过结构体实例调用方法,两者的结果都是一样的。那为什么要多此一举使用接口呢?
接口的作用
如果是像上方那种小规模编程,使用接口确实意义不大,接口是编程规模化之后才起上作用的。接口就是为了抽象,当别人看到一个接口类型时,不知道它具体是做什么的,但可以知道实现它的方法能做什么。
例如,说话 这个抽象概念,当你遇到一个外国人,你听不懂他在说什么,但你知道他在说话。这就好似接口的作用。
接口的数据结构
源码文件:src/runtime/runtime2.go line:205
type iface struct {
tab *itab //指向接口动态类型元数据 和 接口要求的方法列表
data unsafe.Pointer //指向接口的动态值
}
一个 itab 表现为接口的一个实现,是可复用的,所以 Go 语言会将其缓存起来,通过哈希表来存储和查询 itab 信息:
空接口
空接口是非常特殊的接口,所有类型都实现了该接口,所以空接口类型变量可以存放所有值。Go 语言专门为其定义了一个结构体 eface:
源码文件:src/runtime/runtime2.go line:210
type eface struct {
_type *_type //指向接口的动态类型元数据
data unsafe.Pointer //指向接口的动态值
}
3.多态
Go 中的多态性是在接口的帮助下实现的。
多态的定义
多态是同一个操作,作用于不同对象,会有不同的过程和结果。简单来说,就是用相同的接口表示不同的实现。
Go 中的多态
举个例子,就很好理解了:
type Shaper interface {
Area() float64
}
type Square struct {
side float64
}
func (s Square) Area() float64 {
return s.side * s.side
}
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func ComputeArea(shaper Shaper) float64 {
return shaper.Area()
}
func main() {
s := Square{5}
c := Circle{4}
fmt.Println(ComputeArea(s)) // 输出 25
fmt.Println(ComputeArea(c)) // 输出 50.26548245743669
}
4.反射
反射的概念
反射是一种检查interface变量的底层类型(type)和值(value)的机制。
反射三大定律
官方提供了三条定律来说明反射:
一、反射可以将interface类型变量转换成反射对象
提供了两个方法 reflect.TypeOf 和 reflect.ValueOf 来获取到一个变量的反射类型和反射值:
package main
import (
"fmt"
"reflect"
)
func main() {
var a = 1
typea := reflect.TypeOf(a)
valuea := reflect.ValueOf(a)
fmt.Println(typea) //int
fmt.Println(valuea) //1
}
二、反射可以将反射对象还原成interface对象
通过 reflect.Value.Interface 来获取到反射对象的 interface 对象
package main
import (
"fmt"
"reflect"
)
func main() {
var a = 1
valuea := reflect.ValueOf(a)
b := valuea.Interface().(int) //转成interface对象,再通过类型断言获取int类型
fmt.Println(b) //1
}
三、反射对象可修改,value值必须是可设置的
通过 reflect.Value.CanSet 来判断一个反射对象是否是可设置的。
若可设置,可通过 reflect.Value.Set 来修改反射对象的值。(不可设置,强制修改会 panic)
常用到 reflect.Value.Elem 来获取指针指向的值。
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
a := reflect.ValueOf(x)
b := reflect.ValueOf(&x)
fmt.Println(a.CanSet()) // false
fmt.Println(b.CanSet()) // false
fmt.Println(b.Elem().CanSet()) // true
b.Elem().SetFloat(3.14) //如果b的Kind不是Float32或Float64,或者CanSet()为false,会panic
fmt.Println(x) //3.14
}
能够看到,上面只有 b.Elem().CanSet() 为 true,这是 值拷贝 和 指针拷贝 的区别,想要修改对象,这两个是绕不开的。这两者的区别前几篇学习笔记中已经遇到很多了,无非是值拷贝修改副本,指针拷贝修改原变量,不再赘述。
反射的原理
这一部分非常复杂和绕,笔者的功力有限,不知如何写的清楚明白,这里推荐观看 幼麟实验室的视频 ,里面讲解的比较形象通透。