目录

method


method

Go 中的方法是一种特殊类型的函数,但存在一个简单的区别:你必须在函数名称之前加入一个额外的参数。 此额外参数称为“接收方”。

如你希望分组函数并将其绑定到自定义类型,则方法非常有用。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。

如要了解方法在 Go 中的重要性,请先学习如何声明一个方法。

1 声明方法

到目前为止,你仅将结构用作可在 Go 中创建的另一种自定义类型。 在此模块中你将了解到,通过添加方法你可以将行为添加到你所创建的结构中。

声明方法的语法如下所示:

func (variable type) MethodName(parameters ...) {
    // method functionality
}

但是,在声明方法之前,必须先创建结构。 假设你想要创建一个几何包,并决定创建一个名为 triangle 的三角形结构作为此程序包的一个组成部分。 然后,你需要使用一种方法来计算此三角形的周长。 你可以在 Go 中将其表示为:

type triangle struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

结构看起来像普通结构,但 perimeter() 函数在函数名称之前有一个类型 triangle 的额外参数。 此接收方意味着,在使用结构时,你可以按如下方式调用函数:

func main() {
    t := triangle{3}
    fmt.Println("Perimeter:", t.perimeter())
}

如果尝试按平常的方式调用 perimeter() 函数,则此函数将无法正常工作,因为此函数的签名表明它需要接收方。 调用此方法的唯一方式是先声明一个结构,获取此方法的访问权限。 只要此方法属于不同的结构,你甚至可以为其指定相同的名称。 例如,你可以使用 perimeter() 函数声明一个 square 结构,具体如下所示:

package main

import "fmt"

type triangle struct {
    size int
}

type square struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

func (s square) perimeter() int {
    return s.size * 4
}

func main() {
    t := triangle{3}
    s := square{4}
    fmt.Println("Perimeter (triangle):", t.perimeter())
    fmt.Println("Perimeter (square):", s.perimeter())
}

在运行前面的代码时,请注意避免任何错误,你将获得以下输出:

Perimeter (triangle): 9
Perimeter (square): 16

通过对 perimeter() 函数的两次调用,编译器将根据接收方类型来确定要调用的函数。 此行为有助于在各程序包之间保持函数的一致性和名称的简短,并避免将包名称作为前缀。 在下一个单元讲解接口时,我们将介绍此行为的重要性。

2 方法中的指针

有时,方法需要更新变量。 或者,如果方法的参数太大,你可能希望避免复制它。 在遇到此类情况时,你需要使用指针传递变量的地址。 在之前的模块中,当我们在讨论指针时提到,每次在 Go 中调用函数时,Go 都会复制每个参数值以便使用。

如果你需要更新方法中的接收方变量,也会执行相同的行为。 例如,假设你要创建一个新方法以使三角形的大小增加一倍。 你需要在接收方变量中使用指针,具体如下所示:

func (t *triangle) doubleSize() {
    t.size *= 2
}

你可以证明此方法的有效性,具体如下所示:

func main() {
    t := triangle{3}
    t.doubleSize()
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}

在运行前面的代码时,你将看到以下输出:

Size: 6
Perimeter: 18

如果方法仅可访问接收方的信息,则不需要在接收方变量中使用指针。 但是,依据 Go 的约定,如果结构的任何方法具有指针接收方,则此结构的所有方法都必须具有指针接收方。 即使此结构的某个方法不需要它也是如此。

3 声明其他类型的方法

方法的一个关键方面在于,需要为任何类型定义方法,而不只是针对自定义类型(如结构)进行定义。 但是,你不能通过属于其他包的类型来定义结构。 因此,不能在基本类型(如 string)上创建方法。

尽管如此,你仍然可以利用一点技巧,基于基本类型创建自定义类型,然后将其用作基本类型。 例如,假设你要创建一个方法,以将字符串从小写字母转换为大写字母。 你可以按如下所示写入方法:

package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return strings.ToUpper(string(s))
}

func main() {
    s := upperstring("Learning Go!")
    fmt.Println(s)
    fmt.Println(s.Upper())
}

在运行前面的代码时,你将看到以下输出:

Learning Go!
LEARNING GO!

请注意,你在使用新对象 s 时,可以在首次打印其值时将其作为字符串。 然后,你在调用 Upper 方法时,s 会打印出类型字符串的所有大写字母。

4 嵌入方法

在之前的模块中,您已了解到可以在一个结构中使用属性,并将同一属性嵌入另一个结构中。 也就是说,可以重用来自一个结构的属性,以避免出现重复并保持代码库的一致性。 类似的观点也适用于方法。 即使接收方不同,也可以调用已嵌入结构的方法。

例如,假设你想要创建一个带有逻辑的新三角形结构,以加入颜色。 此外,你还希望继续使用之前声明的三角形结构。 因此,彩色三角形结构将如下所示:

type coloredTriangle struct {
    triangle
    color string
}

然后,你可以初始化 coloredTriangle 结构,并从 triangle 结构调用 perimeter() 方法(甚至访问其字段),具体如下所示:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

继续操作并在程序中加入上述更改,以了解嵌入的工作方式。 当使用类似于上一个方法的 main() 方法运行程序时,你将看到以下输出:

Size: 3
Perimeter 9

如果你熟悉 Java 或 C++ 等 OOP 语言,则可能会认为 triangle 结构看起来像基类,而 coloredTriangle 是一个子类(如继承),但事实并不是如此。 实际上,Go 编译器会通过创建如下的包装器方法来推广 perimeter() 方法:

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

请注意,接收方是 coloredTriangle,它从三角形字段调用 perimeter() 方法。 好的一点在于,你不必再创建之前的方法。 你可以选择创建,但 Go 已在内部为你完成了此工作。 我们提供的上述示例仅供学习。

5 重载方法

让我们回到之前讨论过的 triangle 示例。 如果要在 coloredTriangle 结构中更改 perimeter() 方法的实现,会发生什么情况? 不能存在两个同名的函数。 但是,因为方法需要额外参数(接收方),所以,你可以使用一个同名的方法,只要此方法专门用于要使用的接收方即可。 利用这种区别就是重载方法的方式。

换而言之,如你想要更改其行为,可以编写我们讨论过的包装器方法。 如果彩色三角形的周长是普通三角形的两倍,则代码将如下所示:

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

现在,无需更改之前编写的 main() 方法中的任何其他内容,具体将如下所示:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

运行此方法时,你将得到不同输出:

Size: 3
Perimeter 18

但是,如果你仍需要从 triangle 结构调用 perimeter() 方法,则可通过对其进行显示访问来执行此操作,如下所示:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter (colored)", t.perimeter())
    fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}

运行此代码时,应会看到以下输出:

Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

你可能已经注意到,在 Go 中,你可以 替代方法,并在需要时仍访问 原始 方法。

6 方法中的封装

“封装”表示对象的发送方(客户端)无法访问某个方法。 通常,在其他编程语言中,你会将 privatepublic 关键字放在方法名称之前。 在 Go 中,只需使用大写标识符,即可公开方法,使用非大写的标识符将方法设为私有方法。

Go 中的封装仅在程序包之间有效。 换句话说,你只能隐藏来自其他程序包的实现详细信息,而不能隐藏程序包本身。

如要进行尝试,请创建新程序包 geometry 并按如下方式将三角形结构移入其中:

package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}

你可以使用上述程序包,具体如下所示:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
}

此时你应获得以下输出:

Perimeter 18

如要尝试从 main() 函数中调用 size 字段或 doubleSize() 方法,程序将死机,如下所示:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Size", t.size)
    fmt.Println("Perimeter", t.Perimeter())
}

在运行前面的代码时,你将看到以下错误:

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)