go学习记录——第十三天
方法
方法是什么
在 Golang
中,结构体就像是类的一种简化形式,那么面向对象的程序员可能会问类的方法在哪里呢?(反正我是不会问,咱不会Java)在 Golang
中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Golang
方法是作用在接受者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。
接受者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数,可以是 int
bool
string
或 数组的别名类型。但是接收者不能是一个接口类型(参考第十一章),因为接口是一个抽象定义,但是方法却是具体实现的;如果这样做会引发一个编译错误:invalid receiver type...
最后接收者不能是一个指针类型,但它可以是任何其他允许类型的指针。
好的,到这里还是没明白接收者到底能干啥不能干啥。问了下AI然后自己总结吧
首先可以作为接收者的类型
- 结构体类型:
- 最常见的接收者类型。
- 可以定义方法来操作该结构体的字段。
- 非结构体类型(如基本类型、数组、切片等):
- 通过嵌入结构体或定义新的类型别名来实现类似结构体的方法集。
- 命名类型:
- 使用
type
关键字创建的自定义类型。 - 包括基于内置类型的别名。
- 指针类型:
- 指向结构体或其他类型的指针也可以作为接收者。
- 使用指针接收者可以在方法内部修改原始值。
- 函数类型:
- 虽然不太常见,但理论上函数类型也可以有自己的方法。
不可以作为接收者的类型
- 接口类型:
- 接口本身不能直接定义方法,它们只是方法签名的集合。
- 方法必须绑定到具体的实现该接口的具体类型上。
- 字面量类型:
- 如
int
、string
这样的基础数据类型的字面量形式不能单独作为接收者。
- 临时匿名结构体:
- 直接在函数调用中构造的匿名结构体实例不能用作方法的接收者。
代码示例:
正确示例:结构体作为接收者
1 |
|
错误示例:接口作为接收者
1 |
|
好的,继续
一个类型加上它的方法等价于面向对象中的一个类。一个区别是,在 Golang
中,类型的代码和绑定在上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是必须在同一个包中。
类型 T
(或 *T
)上的所有方法的集合叫做类型 T
(或 *T
)的方法集(method set)
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有个给定名称的方法。但是如果基于接收者类型,石油重载的:具有同样名字的方法可以在两个或多个不同的接收者上存在,比如在同一个包里是被允许的。
举个例子来说明
假设我们有两个结构体Circle
和Rectangle
,并且我们想为它们都定义一个计算面积的方法Area
:
1 |
|
在这个例子中:
Circle
类型有一个名为Area
的方法。Rectangle
类型也有一个名为Area
的方法。
尽管这两个方法的名称相同,但由于它们的接收者类型不同(一个是Circle
,另一个是Rectangle
),因此它们被视为两个独立的方法,而不是重载关系。
别名类型没有原始类型上已经定义过的方法。
这里需要特别说明一下,别名类型和原始类型是相互独立的,只会继承结构不会继承方法。
如果想要继承方法是需要手动进行的
就比如我们原始int类型给了一个方法
1 |
|
直接创建别名类型 MyInt
是这样
1 |
|
然后我们手动给别名添加方法如下
1 |
|
定义方法的格式如下
1 |
|
在方法名前,func
关键字后的口号中指定 receiver
。
如果 recv
是 receiver
的实例,Method1
是它的方法名,那么方法调用遵循传统的 object.name
选择器符号:recv.Method1()
。
如果 recv
是一个指针,Go会自动解引用。
如果方法不需要使用 recv
的值,可以用 _
替换它,比如:
1 |
|
recv
就像是面向对象语言中的 this
或 self
,但是 Go 中并没有这两个关键字。随个人喜好,可以使用 this
或 self
作为 receiver
的名字(虽然但是,我完全不觉得会有人想用这个作为receiver的名字)。下面是一个结构体上的简单方法和例子:
示例 method1.go
1 |
|
输出结果
1 |
|
下面是非结构体类型上方法的例子
1 |
|
在练习中有个这个题目
1 |
|
错误就是我们试图在list类型上定义方法,但是我们的list来源不是在这个包内,所以肯定会报错。同样的,我们在给int
float32
或类似这些的类型上定义方法时,就会报类似的错误(虽然但是,书上这句话和这个题目好像没有半点关系啊)
1 |
|
比如想在 time.Time
上定义如下方法:
1 |
|
类似的,在其他的或非本地的包里定义,都会得到上面的错误。
但是有一个间接的方法,可以先定义该类型(比如:int
或 float32(64)
)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效(别名真神奇哦,完全隔离,宛如两个生殖隔离的物种)
1 |
|
输出示例
1 |
|
这边需要说明一下,当我们用 time.Now()
会调用系统时间,所以我们的第二个输出直接就是202,第一个输出因为我们用format
把时间格式化了,所以才能显示我们想要的这个。
函数和方法的区别
函数将变量作为参数: Function1(recv)
方法在变量上被调用: recv.Method1()
在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)
**不要忘记 Method1()
后边的括号 ()
,否则会引发编译器报错: method recv.Method1 is not an expression,must be called
**
接收者必须有一个显式的名字且这个名字可以在方法中被调用。
receiver_type
叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。
在 Go 中,(接收者)类型关联的方法不写在类型结构体里面,就像类那样;耦合更加宽松;类型和方法之间关联由接收者来建立。
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是相互独立的。
感觉讲的好绕啊,我自己捋一遍吧。
总结
可以直接看我这个,前面的不用看也可以(我自己是这么觉得的🤣)
前提:函数和放啊都是用于封装一组操作的代码块,但它们之间存在一些关键区别,尤其是设计接收者时
函数(Function)
定义方法
- 函数使用
func
关键字定义 - 不需要绑定到任何特定的类型
- 函数使用
调用方法
- 直接通过函数名和参数列表调用
- 例如:
Add(3, 8)
接收者
- 函数没有接收者
- 函数独立于任何特定的数据结构
作用域
- 可以在整个包内或外部包中被访问(取决于可见性)
示例
1
2
3func Add(a int, b int) int {
return a + b
}
方法(Method)
定义方式
- 方法也使用
func
关键字定义 - 必须有一个接收者参数,该参数指定了方法关联的类型
- 方法也使用
调用方式
- 通过类型和
.
运算符来调用 - 例如:
num.Add(6)
- 通过类型和
接收者
- 方法有一个或多个接收者,可以是结构体类型、非结构体类型(如基本数据类型、数组、切片等)的别名或接口
- 接收者可以是任何值类型或指针类型
- 值接收者会在方法调用时复制一份数据(两者相互独立,保证原始数据的稳定性,但可能会影响性能),指针接收者会直接操作原始数据
作用域
- 方法通常定义在其关联类型的内部,但也可以在外部定义(只要接收者类型是导出的)
示例
1
2
3
4
5
6
7
8
9
10type MyInt int
func (mi MyInt) Double() MyInt {
return mi * 2
}
// 或者使用指针接收者
func (mi *MyInt) Increment() {
*mi++
}
接收者的作用
- 封装:方法允许我们将相关的行为绑定到特定的类型上,从而实现更好的封装和组织代码,
- 修改状态:通过指针接收者,方法可以直接修改其关联实例的状态
- 多态性:当类型实现了某一个接口的所有方法时,该类型的实例就可以被视为该接口类型,从而实现多态
特性 | 函数 | 方法 |
---|---|---|
定义 | func 关键字,无接收者 |
func 关键字,有接收者 |
调用 | 直接调用 | 通过实例调用 |
接收者 | 无 | 有(值或指针) |
作用域 | 全局或包内 | 类型相关联 |
状态修改 | 不能直接修改调用者状态 | 可以通过指针接收者修改 |
方便对比搞个脑图(试了试ai做的)
指针或值作为接收者
鉴于性能,recv
最常见的是一个指向 receiver_type
的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver
类型是结构体时,就更是如此了。
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
1 |
|
输出结果
1 |
|
上面的例子中 pointer_value.go
,change()
接受了以一个指向 B
的指针,并改变了它的内部成员;write()
通过拷贝接受了 B
的值并只输出 B
的内容。注意 Go 为我们做了探测工作,我们自己并没有指出是否在职镇上调用方法,Go 替我们做了这些事情,b1
是值而 b2
是指针,方法都支持运行了。
捋一下,b1
虽然是一个值但是编译器会自动的将其变为指针进行传递(隐式)而 b2
则是直接进行指针传递(显式),在Go 中,在对方法进行操作的时候我们都是需要进行指针类型的转换,自身已经是则会显式直接进行操作(b2
)而自身是值则会直接由编译器进行一个改变并传递指针(b1
)
试着在 write()
中改变接收者 b
的值:将会看到它可以正常编译,但是开始的 b
没有被改变。
首先我们知道,方法将指针作为接收者不是必须的,如下面的例子,我们只是需要 Point3
的值来做计算:
1 |
|
这样的操作消耗会有点大,因为 Point3
是作为值传递给方法,因此传递的是它的拷贝,这种是可以的,同时也可以是在指向这个类型的指针上调用此方法(会自动解引用)。
假设 p3
定义为一个指针:p3 := &Point{3, 4, 5}
可以使用 p3.Abs()
来代替 (*p3).Abs()
像前面的示例(method1.go)中接受者类型是 *TwoInts
的方法 AddThem()
,它能在类型 TwoInts
的值上被调用,这是自动间接发生的。因此 two2.AddThem
可以替代 (&two2).AddThem()
在值和指针上调用方法:
可以有连接到类型的方法,也可以有连接到指针的方法。
但是这没关系:对于类型 T
,如果在 \*T
上存在方法 Meth()
,并且 t
是这个类型的变量,那么 t.Meth()
会被自动转换为 (&t).Meth()
。
指针方法和脂肪啊都可以在指针或非指针上被调用,如下面的例子所述,类型 List
在值上有一个方法 Len()
,在指针上有一个方法 Append()
,但是可以看到这两个方法都可以在两种类型的变量上被调用。
1 |
|