什么是方法

方法只是一个带有特殊接收器类型的函数,它是在 func 关键字和方法名称之间编写的。接收器可以是结构类型,也可以是非结构类型。

下面是创建方法的语法。


  1. func (t Type) methodName(parameter list) {
  2. }

上面的代码片段创建了一个名为 methodName 的方法,该方法具有接收器类型 Type

例子

让我们编写一个简单的程序,它在结构体类型上创建一个方法并调用它。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. name string
  7. salary int
  8. currency string
  9. }
  10. /*
  11. displaySalary() method has Employee as the receiver type
  12. */
  13. func (e Employee) displaySalary() {
  14. fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
  15. }
  16. func main() {
  17. emp1 := Employee {
  18. name: "Sam Adolf",
  19. salary: 5000,
  20. currency: "$",
  21. }
  22. emp1.displaySalary() //Calling displaySalary() method of Employee type
  23. }

Run program in playground

在上面的程序第 16 行中,我们在 Employee 类型上创建了一个方法 displaySalary。 displaySalary() 方法可以访问其中的接收者 e Employee。第 17 行,我们使用接收器 e 并打印 employee 的 name,currency 和 salary。

第 26 行中,我们使用语法 emp1.displaySalary() 调用了该方法。

程序输出 Salary of Sam Adolf is $5000

方法 vs 函数

以上程序可以用函数重写,不用方法。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. name string
  7. salary int
  8. currency string
  9. }
  10. /*
  11. displaySalary() method converted to function with Employee as parameter
  12. */
  13. func displaySalary(e Employee) {
  14. fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
  15. }
  16. func main() {
  17. emp1 := Employee{
  18. name: "Sam Adolf",
  19. salary: 5000,
  20. currency: "$",
  21. }
  22. displaySalary(emp1)
  23. }

Run program in playground

当我们有函数时,为什么要使用方法?


上面的程序使用函数进行重写。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. name string
  7. salary int
  8. currency string
  9. }
  10. /*
  11. displaySalary() method converted to function with Employee as parameter
  12. */
  13. func displaySalary(e Employee) {
  14. fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
  15. }
  16. func main() {
  17. emp1 := Employee{
  18. name: "Sam Adolf",
  19. salary: 5000,
  20. currency: "$",
  21. }
  22. displaySalary(emp1)
  23. }

Run program in playground

在上面的程序中,displaySalary 方法被转换成一个函数,Employee 结构作为参数传递给它。这个程序也产生了完全相同的输出 Salary of Sam Adolf is $5000

那么,当我们可以使用函数编写相同的程序时,为什么还要使用方法呢?

  • Go不是一种纯面向对象的编程语言,它不支持类。因此,类型上的方法是实现类似类的行为的一种方法。方法允许对与类型相关的行为进行类似于类的逻辑分组。在上面的示例程序中,所有与 Employee 类型相关的行为都可以通过使用 Employee 接收器类型创建方法进行分组。例如,我们可以添加 calculatePension、calculateLeaves 等方法。
  • 具有相同名称的方法可以在不同类型上定义,而不允许使用相同名称的函数。假设我们有一个 SquareCircle 的结构。可以在 SquareCircle 上定义一个名为 Area 的方法。


  1. package main
  2. import (
  3. "fmt"
  4. "math"
  5. )
  6. type Rectangle struct {
  7. length int
  8. width int
  9. }
  10. type Circle struct {
  11. radius float64
  12. }
  13. func (r Rectangle) Area() int {
  14. return r.length * r.width
  15. }
  16. func (c Circle) Area() float64 {
  17. return math.Pi * c.radius * c.radius
  18. }
  19. func main() {
  20. r := Rectangle{
  21. length: 10,
  22. width: 5,
  23. }
  24. fmt.Printf("Area of rectangle %d\n", r.Area())
  25. c := Circle{
  26. radius: 12,
  27. }
  28. fmt.Printf("Area of circle %f", c.Area())
  29. }

Run program in playground

程序输出

  1. Area of rectangle 50
  2. Area of circle 452.389342

方法的上述属性用于接口。在处理接口时,我们将在下一篇教程中讨论这个问题。

指针接收器 vs 值接收器


到目前为止,我们只看到了带有值接收器的方法。我们也可以使用指针接收器创建方法。值和指针接收器之间的区别是,使用指针接收器在方法内部所做的更改对调用者是可见的,而在值接收器中则不是这样。让我们通过一个程序来理解这一点。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. name string
  7. age int
  8. }
  9. /*
  10. Method with value receiver
  11. */
  12. func (e Employee) changeName(newName string) {
  13. e.name = newName
  14. }
  15. /*
  16. Method with pointer receiver
  17. */
  18. func (e *Employee) changeAge(newAge int) {
  19. e.age = newAge
  20. }
  21. func main() {
  22. e := Employee{
  23. name: "Mark Andrew",
  24. age: 50,
  25. }
  26. fmt.Printf("Employee name before change: %s", e.name)
  27. e.changeName("Michael Andrew")
  28. fmt.Printf("\nEmployee name after change: %s", e.name)
  29. fmt.Printf("\n\nEmployee age before change: %d", e.age)
  30. (&e).changeAge(51)
  31. fmt.Printf("\nEmployee age after change: %d", e.age)
  32. }

Run program in playground

在上面的程序中,changeName 方法有一个值接收器 (e Employee),而 changeAge 方法有一个指针接收器 (e *Employee)。调用者不会看到对 changeNameEmployeename 字段所做的更改。因此程序调用方法之前和调用方法 e.changeName("Michael Andrew") 之后会输出相同的结果。因为 changeAge 方法是指针接收器 (e *Employee),所以在方法调用 (&e).changeAge(51) 之后对 age 字段所做的更改将对调用者可见。

这个程序输出

  1. Employee name before change: Mark Andrew
  2. Employee name after change: Mark Andrew
  3. Employee age before change: 50
  4. Employee age after change: 51

上面程序第 36 行,我们使用 (&e).changeAge(51) 来调用 changeAge 方法。因为changeAge 有一个指针接收器,所以我们使用 (&e) 来调用这个方法。这不是必需的,该语言让我们可以选择只使用 e.changeAge(51e.changeAge(51) 会被编译器解释为 (&e).changeAge(51)

下面的程序被重写为使用 e.changeAge(51)而不是 (&e).changeAge(51),并且它打印相同的输出。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Employee struct {
  6. name string
  7. age int
  8. }
  9. /*
  10. Method with value receiver
  11. */
  12. func (e Employee) changeName(newName string) {
  13. e.name = newName
  14. }
  15. /*
  16. Method with pointer receiver
  17. */
  18. func (e *Employee) changeAge(newAge int) {
  19. e.age = newAge
  20. }
  21. func main() {
  22. e := Employee{
  23. name: "Mark Andrew",
  24. age: 50,
  25. }
  26. fmt.Printf("Employee name before change: %s", e.name)
  27. e.changeName("Michael Andrew")
  28. fmt.Printf("\nEmployee name after change: %s", e.name)
  29. fmt.Printf("\n\nEmployee age before change: %d", e.age)
  30. e.changeAge(51)
  31. fmt.Printf("\nEmployee age after change: %d", e.age)
  32. }

Run program in playground

什么时候使用指针接收器,什么时候使用值接收器


通常,当方法内部对接收器所做的更改应该对调用者可见时,可以使用指针接收器。

指针接收器也可以用于复制数据结构开销较大的地方。考虑一个有许多字段的结构。在方法中使用这个结构作为值接收器将需要复制整个结构,开销就会很大。在本例中,如果使用指针接收器,则不会复制结构体,方法中只会使用指向结构体的指针。

在所有其他情况下,都可以使用值接收器。

匿名字段的方法


可以调用属于结构的匿名字段的方法,就像它们属于定义匿名字段的结构一样。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type address struct {
  6. city string
  7. state string
  8. }
  9. func (a address) fullAddress() {
  10. fmt.Printf("Full address: %s, %s", a.city, a.state)
  11. }
  12. type person struct {
  13. firstName string
  14. lastName string
  15. address
  16. }
  17. func main() {
  18. p := person{
  19. firstName: "Elon",
  20. lastName: "Musk",
  21. address: address {
  22. city: "Los Angeles",
  23. state: "California",
  24. },
  25. }
  26. p.fullAddress() //accessing fullAddress method of address struct
  27. }

Run program in playground

在上面程序的第 32 行,我们使用 p.fullAddress())调用 address 结构的fullAddress() 方法。不需要显式 p.address.fullAddress()。这个程序输出

  1. Full address: Los Angeles, California


方法中的值接收器 vs 函数中的值参数


这个话题会让大多数新手感到困惑。我会尽量把它说清楚😀。

当一个函数有一个值参数时,它只接受一个值参数。

当一个方法有一个值接收器时,它将同时接受指针和值接收器。

让我们通过一个例子来理解这一点。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type rectangle struct {
  6. length int
  7. width int
  8. }
  9. func area(r rectangle) {
  10. fmt.Printf("Area Function result: %d\n", (r.length * r.width))
  11. }
  12. func (r rectangle) area() {
  13. fmt.Printf("Area Method result: %d\n", (r.length * r.width))
  14. }
  15. func main() {
  16. r := rectangle{
  17. length: 10,
  18. width: 5,
  19. }
  20. area(r)
  21. r.area()
  22. p := &r
  23. /*
  24. compilation error, cannot use p (type *rectangle) as type rectangle
  25. in argument to area
  26. */
  27. //area(p)
  28. p.area()//calling value receiver with a pointer
  29. }

Run program in playground

程序第 12 行函数 func area(r rectangle) 接受一个值参数,方法 func (r rectangle) area() 接受一个值接收器。

程序第 25 行,我们调用带有值参数 area(r) 的 area 函数,它将工作。类似地,我们使用一个值接收器调用 area 方法 r.area(),也可以工作。

我们程序第 28 行中我们创建一个指向 r 的指针 p。 如果我们尝试将此指针传递给只接受值的函数区,编译器将会报错。程序第 33 行,如果取消注释,则编译器将抛出错误 **compilation error, cannot use p (type rectangle) as type rectangle in argument to area*。这将按预期工作。

现在到了棘手的部分,第 35 行 p.area() 调用方法 area ,该方法接受了使用指针接收器 p 的作为值接收器。这是完全有效的。原因是 p.area() 将被 Go 编译器解释为(*p).area()

该程序将输出

  1. Area Function result: 50
  2. Area Method result: 50
  3. Area Method result: 50


方法中的指针接收器与函数中的指针参数。


与值参数类似,具有指针参数的函数将仅接受指针,而具有指针接收器的方法将接受值和指针接收器。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type rectangle struct {
  6. length int
  7. width int
  8. }
  9. func perimeter(r *rectangle) {
  10. fmt.Println("perimeter function output:", 2*(r.length+r.width))
  11. }
  12. func (r *rectangle) perimeter() {
  13. fmt.Println("perimeter method output:", 2*(r.length+r.width))
  14. }
  15. func main() {
  16. r := rectangle{
  17. length: 10,
  18. width: 5,
  19. }
  20. p := &r //pointer to r
  21. perimeter(p)
  22. p.perimeter()
  23. /*
  24. cannot use r (type rectangle) as type *rectangle in argument to perimeter
  25. */
  26. //perimeter(r)
  27. r.perimeter()//calling pointer receiver with a value
  28. }

Run program in playground

程序的第 12 行定义了一个函数 perimeter ,它接受一个指针参数,第 17 行定义了一种具有指针接收器的方法。

第 27 行我们用指针参数调用 perimete 函数,第 28 行,我们在指针接收器上调用perimete 方法。

在注释行第 33 行中,我们尝试使用值参数 r 调用 perimeter 函数。这是不允许的,因为带有指针参数的函数不接受值参数。如果该行被取消注释并且程序运行,编译将失败,main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter

第 35 行我们用值接收器 r 调用指针接收器方法 perimeter 。这是允许的,代码行r.perimeter() 将被语言解释为 (&r).perimeter()。这个程序将输出

  1. perimeter function output: 30
  2. perimeter method output: 30
  3. perimeter method output: 30


非结构类型上的方法


到目前为止,我们只对结构类型定义了方法。也可以在非结构类型上定义方法,但是有一个问题。要在类型上定义方法,方法的接收者类型的定义和方法的定义应该在同一个包中。到目前为止,我们定义的所有 struct 和 struct 上的方法都位于相同的 main 包中,因此它们都可以工作。

  1. package main
  2. func (a int) add(b int) {
  3. }
  4. func main() {
  5. }

Run program in playground

在上面的程序中,在第 3 行。我们试图在内置的 int 类型上添加一个名为 add 的方法,这是不允许的,因为 add 方法的定义和 int 类型的定义不在同一个包中。此程序将抛出编译错误,cannot define new methods on non-local type int
**
实现此功能的方法是为内置类型 int 创建一个类型别名,然后创建一个以该类型别名作为接收方的方法。

  1. package main
  2. import "fmt"
  3. type myInt int
  4. func (a myInt) add(b myInt) myInt {
  5. return a + b
  6. }
  7. func main() {
  8. num1 := myInt(5)
  9. num2 := myInt(10)
  10. sum := num1.add(num2)
  11. fmt.Println("Sum is", sum)
  12. }

Run program in playground

上面的程序程序第 5 行中,我们为 int 创建了一个别名 myInt 类型。第 7 行我们定义了一个以 myInt 为接收方的 add 方法。

这个程序将输出 Sum is 15

我已经创建了一个仓库,包含了我们到目前为止讨论过的所有概念,它放在了 github上。

原文链接

https://golangbot.com/methods/