在 Go 中定义方法

简介

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的字段。

在其他语言中,方法调用的接收方通常由关键字引用(例如thisself)。Go将接收器视为与其他变量一样的变量,因此您可以随意为其命名。社区对此参数首选的样式是接收者类型的第一个字符的小写版本。在本例中,我们使用了c,因为接收方类型是Creature

main的主体中,我们创建了一个Creature的实例,并为其NameGreeting字段指定了值。我们在这里通过用.连接类型的名称和方法的名称来调用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变量上的方法GreetSayGoodbye,然后使用函数调用风格。

这两种样式输出相同的结果,但使用点符号的示例可读性更好。点链还告诉我们调用方法的顺序,函数样式在哪里颠倒了这个顺序。向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作为其参数之一时,它调用OceanString方法。

如果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

本例定义了一个带有NameOcocuantsBoat类型。我们想强制其他包中的代码通过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.中编码

Published At
Categories with 技术
Tagged with
comments powered by Disqus