简介
Functions允许您将逻辑组织到可重复的过程中,这些过程可以在每次运行时使用不同的参数。在定义函数的过程中,您经常会发现多个函数每次都可能对同一数据块进行操作。GO识别这种模式,并允许您定义称为_方法_的特殊函数,其目的是操作某个特定类型的实例,称为_接收器_。向类型添加方法不仅允许您传达数据是什么,而且还允许您传达应该如何使用该数据。
定义方法
定义方法的语法类似于定义函数的语法。唯一的区别是在unc
关键字之后增加了一个额外的参数,用于指定方法的接收方。接收器是您希望在其上定义方法的类型的声明。下面的示例在结构类型上定义一个方法:
1package main
2
3import "fmt"
4
5type Creature struct {
6 Name string
7 Greeting string
8}
9
10func (c Creature) Greet() {
11 fmt.Printf("%s says %s", c.Name, c.Greeting)
12}
13
14func main() {
15 sammy := Creature{
16 Name: "Sammy",
17 Greeting: "Hello!",
18 }
19 Creature.Greet(sammy)
20}
如果运行此代码,则输出将为:
1[secondary_label Output]
2Sammy says Hello!
我们创建了一个名为Creature
的结构,其中的名称
和Greeting
都是字符串
字段。这个Creature
只定义了一个方法Greet
。在接收方声明中,我们将Creature
的实例赋给了变量c
,以便我们在fmt.Printf
中组装问候消息时可以引用Creature
的字段。
在其他语言中,方法调用的接收方通常由关键字引用(例如this
或self
)。Go将接收器视为与其他变量一样的变量,因此您可以随意为其命名。社区对此参数首选的样式是接收者类型的第一个字符的小写版本。在本例中,我们使用了c
,因为接收方类型是Creature
。
在main
的主体中,我们创建了一个Creature
的实例,并为其Name
和Greeting
字段指定了值。我们在这里通过用.
连接类型的名称和方法的名称来调用Greet
方法。并提供Creature
的实例作为第一个参数。
Go提供了另一种更方便的方法来调用结构实例上的方法,如下例所示:
1package main
2
3import "fmt"
4
5type Creature struct {
6 Name string
7 Greeting string
8}
9
10func (c Creature) Greet() {
11 fmt.Printf("%s says %s", c.Name, c.Greeting)
12}
13
14func main() {
15 sammy := Creature{
16 Name: "Sammy",
17 Greeting: "Hello!",
18 }
19 sammy.Greet()
20}
如果运行此命令,输出将与上一个示例相同:
1[secondary_label Output]
2Sammy says Hello!
此示例与上一个示例相同,但这次我们使用了_dotnotation_来调用Greet
方法,使用存储在sammy
变量中的Creature
作为接收方。这是第一个示例中函数调用的简写表示法。标准库和围棋社区非常喜欢这种风格,以至于您很少看到前面所示的函数调用风格。
下一个例子展示了点符号更流行的一个原因:
1package main
2
3import "fmt"
4
5type Creature struct {
6 Name string
7 Greeting string
8}
9
10func (c Creature) Greet() Creature {
11 fmt.Printf("%s says %s!\n", c.Name, c.Greeting)
12 return c
13}
14
15func (c Creature) SayGoodbye(name string) {
16 fmt.Println("Farewell", name, "!")
17}
18
19func main() {
20 sammy := Creature{
21 Name: "Sammy",
22 Greeting: "Hello!",
23 }
24 sammy.Greet().SayGoodbye("gophers")
25
26 Creature.SayGoodbye(Creature.Greet(sammy), "gophers")
27}
如果你运行这段代码,输出看起来像这样:
1[secondary_label Output]
2Sammy says Hello!!
3Farewell gophers !
4Sammy says Hello!!
5Farewell gophers !
我们修改了前面的示例,引入了另一个名为SayGoodbye
的方法,并将Greet
更改为返回一个Creature
,这样我们就可以在该实例上调用更多的方法。在main
的主体中,我们首先使用点符号调用sammy
变量上的方法Greet
和SayGoodbye
,然后使用函数调用风格。
这两种样式输出相同的结果,但使用点符号的示例可读性更好。点链还告诉我们调用方法的顺序,函数样式在哪里颠倒了这个顺序。向SayGoodbye
调用添加参数进一步模糊了方法调用的顺序。点符号的清晰性是在GO中调用方法的首选样式,无论是在标准库中还是在GO生态系统中的第三方包中都是如此。
在类型上定义方法,而不是定义对某些值进行操作的函数,对Go编程语言具有其他特殊的意义。方法是接口背后的核心概念。
接口
当您在Go中的任何类型上定义方法时,该方法将被添加到该类型的_方法集_。方法集是与该类型相关联的函数的集合,作为方法,由GO编译器用来确定是否可以将某个类型赋给具有接口类型的变量。接口类型是编译器用来保证类型为这些方法提供实现的方法规范。任何类型的方法与接口定义中的方法具有相同的名称、相同的参数和相同的返回值,则该类型称为_IMPLEMENT_该接口,并允许将其赋给具有该接口类型的变量。下面是标准库中fmt.Stringer
接口的定义:
1type Stringer interface {
2 String() string
3}
一个类型要实现fmt.Stringer
接口,需要提供一个String()
方法,该方法返回一个字符串
。当您将类型的实例传递给fmt
包中定义的函数时,实现此接口将允许完全按照您的意愿打印您的类型(有时称为)。下面的示例定义实现此接口的类型:
1package main
2
3import (
4 "fmt"
5 "strings"
6)
7
8type Ocean struct {
9 Creatures []string
10}
11
12func (o Ocean) String() string {
13 return strings.Join(o.Creatures, ", ")
14}
15
16func log(header string, s fmt.Stringer) {
17 fmt.Println(header, ":", s)
18}
19
20func main() {
21 o := Ocean{
22 Creatures: []string{
23 "sea urchin",
24 "lobster",
25 "shark",
26 },
27 }
28 log("ocean contains", o)
29}
当你运行代码时,你会看到这样的输出:
1[secondary_label Output]
2ocean contains : sea urchin, lobster, shark
本例定义了一个名为Ocean
的新结构类型。之所以说Ocean
是_Implementate_thefmt.Stringer
接口,是因为Ocean
定义了一个名为String
的方法,该方法不带参数,返回一个字符串
。在main
中,我们定义了一个新的Ocean
,并将其传递给一个log
函数,该函数首先使用一个字符串
来打印输出,然后是实现fmt.Stringer
的任何内容。Go编译器允许我们在这里传递o
,因为Ocean
实现了fmt.Stringer
请求的所有方法。在log
中,我们使用fmt.Println
,当它遇到fmt.Stringer
作为其参数之一时,它调用Ocean
的String
方法。
如果Ocean
没有提供String()
方法,则Go会产生编译错误,因为log
方法请求fmt.Stringer
作为参数。错误如下所示:
1[secondary_label Output]
2src/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
3 Ocean does not implement fmt.Stringer (missing String method)
Go还将确保提供的String()
方法与fmt.Stringer
接口请求的方法完全匹配。如果不这样做,它将生成如下所示的错误:
1[secondary_label Output]
2src/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
3 Ocean does not implement fmt.Stringer (wrong type for String method)
4 have String()
5 want String() string
在到目前为止的示例中,我们已经在值接收器上定义了方法。也就是说,如果我们使用方法的函数调用,则引用定义方法的类型的第一个参数将是该类型的值,而不是pointer.因此,当方法完成执行时,我们对提供给方法的实例所做的任何修改都将被丢弃,因为收到的值是数据的副本。还可以将指针接收器上的方法定义为类型。
指针接收器
在指针接收器上定义方法的语法与在值接收器上定义方法的语法几乎相同。不同之处在于接收器声明中的类型名称前面加了一个星号(*
)。下面的示例将指针接收器上的方法定义为一个类型:
1package main
2
3import "fmt"
4
5type Boat struct {
6 Name string
7
8 occupants []string
9}
10
11func (b *Boat) AddOccupant(name string) *Boat {
12 b.occupants = append(b.occupants, name)
13 return b
14}
15
16func (b Boat) Manifest() {
17 fmt.Println("The", b.Name, "has the following occupants:")
18 for _, n := range b.occupants {
19 fmt.Println("\t", n)
20 }
21}
22
23func main() {
24 b := &Boat{
25 Name: "S.S. DigitalOcean",
26 }
27
28 b.AddOccupant("Sammy the Shark")
29 b.AddOccupant("Larry the Lobster")
30
31 b.Manifest()
32}
运行此示例时,您将看到以下输出:
1[secondary_label Output]
2The S.S. DigitalOcean has the following occupants:
3 Sammy the Shark
4 Larry the Lobster
本例定义了一个带有Name
和Ococuants
的Boat
类型。我们想强制其他包中的代码通过AddOccuant
方法只添加Ococants,所以我们通过降低字段名的第一个字母来使Ococants
字段不能导出。我们还想确保调用AddOccuant
会导致Boat
的实例被修改,这就是为什么我们在指针接收器上定义了AddOccuant
。指针充当对类型的特定实例的引用,而不是该类型的副本。知道将使用指向Boat
的指针调用AddOccuant
,可以保证任何修改都将持续存在。
在main
中,我们定义了一个新变量b
,它将保存指向Boat
(* Boat
)的指针。我们在该实例上两次调用AddOccuant
方法来添加两个乘客。Manifest
方法定义在Boat
值上,因为在它的定义中,接收方被指定为(B Boat)
。在main
中,我们仍然可以调用Manifest
,因为Go能够自动取消对指针的引用,以获得Boat
值。b.Manifest()
相当于(* b).Manifest()
。
在尝试为接口类型的变量赋值时,方法是定义在指针接收器上还是定义在值接收器上具有重要意义。
指针接收器和接口
当你给一个具有接口类型的变量赋值时,Go编译器会检查被赋值的类型的方法集,以确保它具有接口期望的方法。指针接收器和值接收器的方法集是不同的,因为接收指针的方法可以修改它们的接收器,而接收值的方法不能。
下面的示例演示如何定义两个方法:一个在类型的指针接收器上,另一个在它的值接收器上。但是,只有指针接收器才能满足本例中定义的接口:
1package main
2
3import "fmt"
4
5type Submersible interface {
6 Dive()
7}
8
9type Shark struct {
10 Name string
11
12 isUnderwater bool
13}
14
15func (s Shark) String() string {
16 if s.isUnderwater {
17 return fmt.Sprintf("%s is underwater", s.Name)
18 }
19 return fmt.Sprintf("%s is on the surface", s.Name)
20}
21
22func (s *Shark) Dive() {
23 s.isUnderwater = true
24}
25
26func submerge(s Submersible) {
27 s.Dive()
28}
29
30func main() {
31 s := &Shark{
32 Name: "Sammy",
33 }
34
35 fmt.Println(s)
36
37 submerge(s)
38
39 fmt.Println(s)
40}
运行代码时,您将看到以下输出:
1[secondary_label Output]
2Sammy is on the surface
3Sammy is underwater
此示例定义了一个名为Submersible
的接口,该接口要求类型具有dive()
方法。然后,我们定义了一个带有Name
字段和一个isUnderwater
方法的Shark
类型来跟踪Shark
的状态。我们在指针接收器上为Shark
定义了一个dive()
方法,将isUnderwater
修改为true
。我们还定义了值接收器的String()
方法,以便它可以通过使用前面介绍的fmt.Println
接受的fmt.Stringer
接口,使用fmt.Println
干净地打印Shark
的状态。我们还使用了接受Submersible
参数的函数submerge
。
使用Submersible
接口而不是* Shark
允许submerge
函数仅依赖于类型提供的行为。这使得submerge
函数更具可重用性,因为您不必为‘Submarine’、‘Whale’或任何我们还没有想到的未来水生生物编写新的submerge
函数。只要它们定义了一个dive()
方法,就可以与submerge
函数一起使用。
在main
中,我们定义了一个变量s
,它是指向Shark
的指针,并立即用fmt.Println
打印了s
。这显示了输出的第一部分,‘Sammy is on the Surface’。我们将s
传递给submerge
,然后以s
为参数再次调用fmt.Println
,以看到输出的第二部分打印出来,`Sammy is Unwater‘。
如果将s
更改为Shark
而不是* Shark
,Go编译器将产生错误:
1[secondary_label Output]
2cannot use s (type Shark) as type Submersible in argument to submerge:
3 Shark does not implement Submersible (Dive method has pointer receiver)
Go编译器告诉我们Shark有一个Dive方法,它只是定义在指针接收器上。当您在自己的代码中看到此消息时,解决方法是在赋值类型的变量之前使用&
运算符传递指向接口类型的指针。
结论
在GO中声明方法最终与定义接收不同类型变量的函数没有什么不同。Working With pointers]的相同规则也适用。GO为这个非常常见的函数定义提供了一些便利,并将它们收集到一组方法中,这些方法可以通过接口类型进行推理。有效地使用方法将允许您使用代码中的接口来提高可测试性,并为代码的未来读者留下更好的组织。
如果您想了解更多关于Go编程语言的一般信息,请查看我们的如何在Go series.中编码