Golang学习笔记(六)—— 面向对象
面向对象
面向对象有三大特点:封装、继承和多态
方法
给内置类型定义方法是不被允许的
接收者
接口类型是无效的方法接收者
在之前的 学习笔记(二) 中有提到过方法,其格式如下:
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。
既然知道了方法变量是闭包,那就需要注意:闭包的捕获列表可能只进行值拷贝,也可能造成变量逃逸。这些在之前的 函数进阶 中有讨论过,这里就不在赘述。
接口
在了解接口前,不得不先理解 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 //指向接口的动态值 }
多态
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 }
反射
反射的概念
反射是一种检查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().CanSet() 为 true,这是 值拷贝 和 指针拷贝 的区别,想要修改对象,这两个是绕不开的。这两者的区别前几篇学习笔记中已经遇到很多了,无非是值拷贝修改副本,指针拷贝修改原变量,不再赘述。
反射的原理
这一部分非常复杂和绕,笔者的功力有限,不知如何写的清楚明白,这里推荐观看 幼麟实验室的视频 ,里面讲解的比较形象通透。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现