Go语言方法和接收器、为任意类型添加方法
参考文档
1 前言
方法:
方法就是作用在接收器(receiver)上的一个函数;
接收器:
某个类型的变量;
为什么有函数了还需要方法呢?
Go不是纯粹的面向对象编程语言,而且Go不支持类。因此基于类型的方法是一种实现和类相似行为的途径相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。
2 介绍
在Go语言中,结构体就像是其他语言中类的一种简化形式,那么类的方法在哪里呢?在Go语言中有一个概念,它和方法有着同样的名字,并且大体上意思相同,Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。(简述:Go语言中的结构体是类的简化,Go作用在接收器(receiver)上的一个函数即是方法,它和其他语言中类的方法是一样的。
)
接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误 invalid receiver type…
。(简述:接收器的类型除了不能为接口类型外,其他类型均可。
)
一个类型加上它的方法等价于面向对象中的一个类,一个重要的区别是,在Go语言中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的。
类型 T(或 T)上的所有方法的集合叫做类型 T(或 T)的方法集。
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法,但是如果基于接收器类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收器类型上存在,比如在同一个包里这么做是允许的。
提示:
在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在Go语言中“方法”的概念与其他语言一致,只是Go语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。
3 Go语言方法和接收器
3.1 为结构体添加方法
使用背包作为“对象”,将物品放入背包的过程作为“方法”,通过面向过程的方式和Go语言中结构体的方式来理解“方法”的概念。
3.1.1 面向过程实现方法
面向过程中没有“方法”概念,只能通过结构体和函数,由使用者使用函数参数和调用关系来形成接近“方法”的概念,代码如下:
package main
type Bag struct {
items []int
}
func Insert(b *Bag, itemId int) {
b.items = append(b.items, itemId)
}
func main() {
bag :=new(Bag)
Insert(bag,1001)
}
代码说明如下:
- 第 3 行,声明 Bag 结构,这个结构体包含一个整型切片类型的 items 的成员。
- 第 7 行,定义了 Insert() 函数,这个函数拥有两个参数,第一个是背包指针(*Bag),第二个是物品 ID(itemid)。
- 第 8 行,用 append() 将 itemid 添加到 Bag 的 items 成员中,模拟往背包添加物品的过程。
- 第 12 行,创建背包实例 bag。
- 第 13 行,调用 Insert() 函数,第一个参数放入背包,第二个参数放入物品 ID。
Insert() 函数将 *Bag 参数放在第一位,强调 Insert 会操作 *Bag 结构体,但实际使用中,并不是每个人都会习惯将操作对象放在首位,一定程度上让代码失去一些范式和描述性。同时,Insert() 函数也与 Bag 没有任何归属概念,随着类似 Insert() 的函数越来越多,面向过程的代码描述对象方法概念会越来越麻烦和难以理解。
3.1.2 Go语言的结构体方法
将背包及放入背包的物品中使用Go语言的结构体和方法方式编写,为 *Bag 创建一个方法,代码如下:
package main
type Bag struct {
items []int
}
func (b *Bag) Insert(itemId int) {
b.items = append(b.items, itemId)
}
func main() {
bag := new(Bag)
bag.Insert(1001)
}
第 7 行中,Insert(itemid int) 的写法与函数一致,(b*Bag) 表示接收器,即 Insert 作用的对象实例。
第 13 行中,在 Insert() 转换为方法后,我们就可以愉快地像其他语言一样,用面向对象的方法来调用 b 的 Insert。
每个方法只能有一个接收器,如下图所示。
3.2 接收器——方法作用的目标
接收器的格式如下:
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
函数体
}
对各部分的说明:
- 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
- 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:格式与函数定义一致。
接收器根据接收器的类型可以分为指针接收器
、非指针接收器
,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。
3.2.1 理解指针类型的接收器
指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。
由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的
。
在下面的例子,使用结构体定义一个属性(Property),为属性添加 SetValue() 方法以封装设置属性的过程,通过属性的 Value() 方法可以重新获得属性的数值,使用属性时,通过 SetValue() 方法的调用,可以达成修改属性值的效果。
package main
import "fmt"
// 定义属性结构
type Property struct {
value int // 属性值
}
// 设置属性值
func (p *Property) SetValue(v int) {
// 修改p的成员变量
p.value = v
}
// 取属性值
func (p *Property) Value() int {
return p.value
}
func main() {
// 实例化属性
p := new(Property)
// 设置值
p.SetValue(100)
// 打印值
fmt.Println(p.Value())
}
运行程序,输出如下:
100
代码说明如下:
- 第 6 行,定义一个属性结构,拥有一个整型的成员变量。
- 第 11 行,定义属性值的方法。设置属性值方法的接收器类型为指针,因此可以修改成员值,即便退出方法,也有效。
- 第 17 行,定义获取值的方法。
- 第 22 行,实例化属性结构。
- 第 24 行,设置值,此时成员变量变为 100。
- 第 26 行,获取成员变量。
3.2.2 理解非指针类型的接收器
当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效
。
点(Point)使用结构体描述时,为点添加 Add() 方法,这个方法不能修改 Point 的成员 X、Y 变量,而是在计算后返回新的 Point 对象,Point 属于小内存对象,在函数返回值的复制过程中可以极大地提高代码运行效率,详细过程请参考下面的代码。
package main
import (
"fmt"
)
// 定义点结构
type Point struct {
X int
Y int
}
// 非指针接收器的加方法
func (p Point) Add(other Point) Point {
// 成员值与参数相加后返回新的结构
return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
// 初始化点
p1 := Point{1, 1}
p2 := Point{2, 2}
// 与另外一个点相加
result := p1.Add(p2)
// 输出结果
fmt.Println(result)
}
代码输出如下:
{3 3}
代码说明如下:
- 第 8 行,定义一个点结构,拥有 X 和 Y 两个整型分量。
- 第 14 行,为 Point 结构定义一个 Add() 方法,传入和返回都是点的结构,可以方便地实现多个点连续相加的效果,例如P4 := P1.Add( P2 ).Add( P3 )
- 第 20 和 21 行,初始化两个点 p1 和 p2。
- 第 23 行,将 p1 和 p2 相加后返回结果。
- 第 25 行,打印结果。
由于例子中使用了非指针接收器,Add() 方法变得类似于只读的方法,Add() 方法内部不会对成员进行任何修改。
3.2.3 指针和非指针接收器的使用
在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。
4 为任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}