17、go中的方法

73 阅读4分钟

本文为译文 Methods in Go

一个method就是一个函数在func关键字和method name之间有一个特殊的receiver。这个receiver可以是结构体类型,也可以是非结构体类型。

func (t Type) methodName(parameter list) {  
}

上面的代码为Type类型创建了一个methodName方法。其中变量t可以在方法内部被访问到。

package main

import (  
    "fmt"
)

type Employee struct {  
    name     string
    salary   int
    currency string
}

/*
 displaySalary() method has Employee as the receiver type
*/
func (e Employee) displaySalary() {  
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {  
    emp1 := Employee {
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    emp1.displaySalary() //Calling displaySalary() method of Employee type
}

在第16行,我们在Empolyee类型上创建了一个displaySalary方法。

Methods vs Functions

上面的程序我们也可以单独写成函数的形式。

package main

import (  
    "fmt"
)

type Employee struct {  
    name     string
    salary   int
    currency string
}

/*
 displaySalary() method converted to function with Employee as parameter
*/
func displaySalary(e Employee) {  
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {  
    emp1 := Employee{
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    displaySalary(emp1)
}

上面的程序和我们使用方法的时候,输出是完全一样的。所以,为啥我们还需要有方法的这个概念呢?然我们一块来梳理一下

  • 首先,go不是一个很纯粹的面向对象编程的语言,它不支持类。因此,类型上的方法是一种去实现了类功能差不多的形式。方法允许对相关的行为进行分组。拿上面的例子来说,所有和Employee类型相关的可以通过方法组织在一起。比如我们可以添加方法calculatePension``calculateLeaves

  • 有相同名字的方法可以被定义为不同的类型。但是函数就不可以。假设我们有Square``Circle两个结构。这两个结构可能都会有一个Area方法。我们的程序可以这么写

    package main

    import (
    "fmt" "math" )

    type Rectangle struct {
    length int width int }

    type Circle struct {
    radius float64 }

    func (r Rectangle) Area() int {
    return r.length * r.width }

    func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius }

    func main() {
    r := Rectangle{ length: 10, width: 5, } fmt.Printf("Area of rectangle %d\n", r.Area()) c := Circle{ radius: 12, } fmt.Printf("Area of circle %f", c.Area()) }

    Area of rectangle 50
    Area of circle 452.389342

指针接收器vs值接收器

上面的程序我们看见的都是值接收器。指针接收器与值接收器不通的是,其在函数内部的修改会反映在外边。

package main

import (  
    "fmt"
)

type Employee struct {  
    name string
    age  int
}

/*
Method with value receiver  
*/
func (e Employee) changeName(newName string) {  
    e.name = newName
}

/*
Method with pointer receiver  
*/
func (e *Employee) changeAge(newAge int) {  
    e.age = newAge
}

func main() {  
    e := Employee{
        name: "Mark Andrew",
        age:  50,
    }
    fmt.Printf("Employee name before change: %s", e.name)
    e.changeName("Michael Andrew")
    fmt.Printf("\nEmployee name after change: %s", e.name)

    fmt.Printf("\n\nEmployee age before change: %d", e.age)
    (&e).changeAge(51)
    /**
    use e.changeAge(51) instead of (&e).changeAge(51) 
     and it prints the same output
    */
    fmt.Printf("\nEmployee age after change: %d", e.age)
}

Employee name before change: Mark Andrew  
Employee name after change: Mark Andrew

Employee age before change: 50  
Employee age after change: 51 

在36行,我们使用(&e).changeAge(51)去调用changeAge方法。其实我们也可以这样写e.changeAge(51)go会自动把这个变成(&e).changeAge(51)

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

  1. 当你期望当方法内对数据的变化,想同步体现到调用者的时候。
  2. 当复制数据结构比较昂贵的时候,也推荐用指针接收器。比如一个结构体有很多字段,如果用值接收器则会将整个结构体做复制,如果数据量比较大的话,代价就是很高的。如果使用”指针接收器“则不会有复制的操作了。

剩余的其他情况,都建议使用值接收器。

匿名结构体字段的方法

package main

import (  
    "fmt"
)

type address struct {  
    city  string
    state string
}

func (a address) fullAddress() {  
    fmt.Printf("Full address: %s, %s", a.city, a.state)
}

type person struct {  
    firstName string
    lastName  string
    address
}

func main() {  
    p := person{
        firstName: "Elon",
        lastName:  "Musk",
        address: address {
            city:  "Los Angeles",
            state: "California",
        },
    }

    p.fullAddress() //accessing fullAddress method of address struct

}

In line no. 32 of the program above, we call the fullAddress() method of the address struct using p.fullAddress(). The explicit direction p.address.fullAddress() is not needed.

方法的值接收器VS函数的值参数

package main

import (  
    "fmt"
)

type rectangle struct {  
    length int
    width  int
}

func area(r rectangle) {  
    fmt	.Printf("Area Function result: %d\n", (r.length * r.width))
}

func (r rectangle) area() {  
    fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}

func main() {  
    r := rectangle{
        length: 10,
        width:  5,
    }
    area(r)
    r.area()

    p := &r
    /*
       compilation error, cannot use p (type *rectangle) as type rectangle 
       in argument to area  
    */
    //area(p)

    p.area()//calling value receiver with a pointer
}

最后一行 **p.area()**能工作的原因,这句话会被go解释为***(p).area()**因为**area**接受的是值类型。

方法的指针接收器VS函数的指针接收器

相似的,方法的指针接收器既能接受值也能接受指针。函数就只能接受指针了,即写啥类型接受啥类型。

package main

import (  
    "fmt"
)

type rectangle struct {  
    length int
    width  int
}

func perimeter(r *rectangle) {  
    fmt.Println("perimeter function output:", 2*(r.length+r.width))

}

func (r *rectangle) perimeter() {  
    fmt.Println("perimeter method output:", 2*(r.length+r.width))
}

func main() {  
    r := rectangle{
        length: 10,
        width:  5,
    }
    p := &r //pointer to r
    perimeter(p)
    p.perimeter()

    /*
        cannot use r (type rectangle) as type *rectangle in argument to perimeter
    */
    //perimeter(r)

    r.perimeter()//calling pointer receiver with a value

}

最后一行可以的原因也是go会把**r.perimeter()**转换为**(&r).perimeter()**

Methods with non-struct receivers

以上我们定义了结构体类型的方法。我们也可以定义非结构体的方法。去定义一个类型的方法,接受类型的定义和方法的定义应该存在在相同的包中。

package main

func (a int) add(b int) {  
}

func main() {

}

上面的程序会报错,因为方法add和类型内置类型int不在一个包里。改成下面就可以生效了。

package main

import "fmt"

type myInt int

func (a myInt) add(b myInt) myInt {  
    return a + b
}

func main() {  
    num1 := myInt(5)
    num2 := myInt(10)
    sum := num1.add(num2)
    fmt.Println("Sum is", sum)
}