[译] part 17: golang 方法 methods

https://juejin.im/post/5cab05996fb9a05e396a621f

什么是方法

方法是一个具有特殊接收者类型的函数,接收者在func关键字和方法名称之间。接收者可以是struct类型或非struct类型。接收者可用于方法内部的访问。

以下是创建方法的语法。

  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

Run in playgroud

在上面程序中的第 16 行,我们在Employee结构类型上创建了一个方法displaySalarydisplaySalary()方法可以访问其中的接收者e Employee。在第 17 行,我们使用接收者e并打印员工的姓名,币种和工资。

在第 26 行,我们使用语法emp1.displaySalary()调用了该方法,程序打印了,Salary of Sam Adolf is $5000

有了函数为啥还需要方法

我们仅使用函数来重写上面的程序。

  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 in playgroud

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

那么既然函数能实现一样的功能,为什么还需要方法呢。这有几个原因。让我们逐一看看它们。

  • Go 不是纯粹的面向对象编程语言,它不支持类。因此,类型上的方法是一种实现类似于类的行为的方法。
  • 不同类型上可以定义具有相同名称的方法,而函数则不允许具有相同名称。让我们假设我们有一个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 in playgroud

程序输出,

  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 in playgroud

在上面的程序中,changeName方法有一个值接收者(e Employee),而changeAge方法有一个指针接收者(e * Employee)。对changeName中的Employee结构的名称字段所做的更改将对调用者不可见,因此程序在调用方法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(51)的选项。 在指针接收者的情况下,使用e.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 in playgroud

什么时候使用指针接收者&什么时候使用值接收者

通常,当调用者需要对方法所做的修改可见时,可以使用指针接收者。

指针接收者也可用于复制数据结构代价比较高的的地方。考虑一个包含许多字段的结构。使用此结构作为方法中的值接收者将需要复制整个结构,这代价是很高的。在这种情况下,如果使用指针接收者,则不会复制结构,并且只在该方法中使用指向它的指针。

在其他情况下,可以使用值接收者。

匿名字段的方法

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

  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 in playgroud

在上面程序的第 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 in playgroud

第 12 行中的函数func area(r rectangle)接受值参数,方法func(r rectangle) area()接受值接收者。

第 25 行,我们使用值参数area(r)调用 area 函数。类似地,我们使用值接收者调用 area 方法r.area()

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

现在是棘手的部分,在第 35 行中,代码p.area()中使用指针接收者p调用值接受者的方法 area,这完全有效。因为 area 有一个值接收者,为方便起见,Go 会把p.area()解析成(* p).area()

程序会输出,

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

方法中的指针接收者 VS 函数的指针参数

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

  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 in playgroud

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

在第 27 行,我们用指针参数调用perimeter函数。在第 28 行,我们用指针接受者调用perimeter方法。

在注释行第 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

非结构类型的方法

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

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

Run in playgroud

在上面的程序中的第 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 in playgroud

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

程序将输出Sum is 15.

ft_authoradmin  ft_create_time2019-08-03 16:35
 ft_update_time2019-08-03 16:36