方法

什么是方法

Golang中,方法是一种定义在某个接收器上的函数,其中接收器位于关键字func和方法名之间。 接收器其实很简单,只是接收器这个名字听起来比较晦涩难懂。在Golang中我们定义方法跟其他编程语言其实很类似,也是为了给某个数据类型来提供一些功能性方法,Golang采用的方式就是接收器。Golang中接收器既可以是结构体类型,也可以是非结构体类型。在方法内部我们可以访问接收器,这就像在JavaJavaScript等编程语言中我们可以在方法内部访问this一样。

Golang中创建一个方法的语法如下:

func (t Type) methodName(parameter list) {

}

这里创建了一个名为methodName的方法,该方法的接收器数据类型为Type

方法举例

下面我们为某个结构体类型创建一个方法:

package main

import "fmt"

type Employee struct {
    name string
    salary int
    currency string
}

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()
}

这里我们在结构体数据类型Employee上创建了一个方法displaySalary。在该方法内部我们可以通过接收器e Employee来访问调用该方法的结构体变量,因此我们可以将接收器理解为该方法执行的上下文,类似于面向对象编程语言比如Java中的this。这里我们通过接收器e来打印了一个员工的名字和薪资。创建完方法之后我们通过emp1.displaySalary()来调用该方法。该程序打印结果如下:

Salary of Sam Adolf is $5000

方法 vs 函数

Golang中既然已经有函数了,为什么还需要方法呢?我们完全可以使用函数来实现前段代码的功能:

package main

import "fmt"

type Employee struct {
    name string
    salary int
    currency string
}

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)
}

这里我们没采用方法,而是使用函数实现了相同的功能,打印结果为:Salary of Sam Adolf is $5000。 因此我们肯定要问,为什么还要在Golang中使用方法呢?其实方法在Golang中还是很重要的,主要有以下几点原因:

  • Golang不是一种纯粹的面向对象编程语言,Golang并不支持class。我们可以通过方法来实现面向对象的特性,后面几章节我们会详细介绍Golang中的面向对象编程,现在你只需要知道Golang中面向对象编程需要借助方法即可。

  • Golang中不同的数据类型可以定义相同的方法名称。假设两个结构体类型分别是SquareCircle。我们可以分别在这两个结构体类型上定义Area方法:

    ```go

    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())

}

[](https://play.golang.org/p/w-k3IvTQIpT)
该程序输出结果为:
```console
Area of rectangle 50
Area of circle 452.389342

指针接收器 vs 值接收器

前面我们实现的方法都是基于值接收器,Golang中也可以基于指针接收器来实现方法。这两种不同接收器的区别在于,基于指针接收器实现的方法,我们可以通过指针来改变该指针指向的变量,而这可能对方法之外定义的变量产生直接影响。而基于值接收器实现的方法,在方法内部相当于对某个变量做了一个拷贝,因此不会对原变量产生直接影响。 这一点看起来比较晦涩,下面我们通过一个例子来理解。

package main

import "fmt"

type Employee struct {
    name string
    age  int
}

func (e Employee) changeName(newName string) {
    e.name = newName
}

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)
    fmt.Printf("\nEmployee age after change: %d", e.age)
}

该程序中,方法changeName使用的是值接收器(e Employee),而方法changeAge使用的是指针接收器(e *Employee)。在changeName方法内部对结构体变量的修改e.changeName("Michael Andrew")不会影响之前定义好的结构体变量e的属性值e.name,但是在changeAge方法内部通过指针对结构体变量的修改(&e).changeAge(51)会直接反应到之前定义好的结构体变量e的属性值e.age上。该程序输出如下:

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

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

前面程序中我们使用(&e).changeAge(51)来调用changeAge方法,这是因为方法changeAge的接收器类型是指针接收器,因此我们需要基于(&e)来调用。不过Golang给我们提供了一种便利的语法糖,我们可以直接使用e.changeAge(51)来调用,Golang会检测到changeAge方法的接收器类型是指针接收器,因此会帮我们把e.changeAge(51)转换为(&e).changeAge(51)

那么何时使用值接收器,何时又使用指针接收器呢?其实很简单,这本质上还是指针传递和值传递的问题。如果你不想通过指针来改变方法外部定义的变量,那么就使用值传递,否则就使用指针传递。但是用值传递会将变量整体拷贝一份,对于比较大的结构体变量开销比较大,这种情况下考虑到性能可以使用指针接收器。

结构体匿名属性的方法

对于结构体的匿名属性,定义在该匿名属性上的方法可以直接在所属的结构体变量上调用。看起来比较晦涩难懂,我们通过一个例子加以理解。

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
}

这里,我们通过p.fullAddress()来调用定义在其匿名属性address上的方法fullAddress(),这等价于p.address.fullAddress()。该程序输出如下:

Full address: Los Angeles, California

方法调用语法糖

前面我们已经看到了,如果一个方法的接收器类型是指针接收器,那么我们可以直接通过变量来调用方法e.changeAge(51),Golang会帮我们转换成(&e).changeAge(51),这其实是Golang提供给我们的语法糖。

Golang还提供了另外一种语法糖,如果一个方法的接收器类型是值接收器,那么正常情况下我们调用该方法是通过e.methodName(),其中e是一个结构体变量。但是如果我们拿到的是指针,也可以通过指针来调用:

p := &e // 获取结构体变量e的指针
p.methodName() // 通过指针来调用

可以看出,Golang提供了非常方便的语法糖,Golang去判断具体调用的方法接收器类型是指针接收器还是值接收器,然后帮我们做转换。需要注意,这种语法糖仅适用于方法,不适用于函数,如果一个函数声明的入参类似是指针类型,那么仅能传递指针类型的参数,反之如果函数声明的入参类型是值类型,也仅能传递值类型的参数。

方法定义与类型声明必须同包

在Golang中,如果我们要给某个结构体类型定义一个方法,那么该方法的定义需要与类型的定义位于同一个包中。 前面的几个例子我们都是这么做的,因此没什么问题,否则就会报类似如下的错误cannot define new methods on non-local type xxx

给非结构体类型定义方法

在Golang中我们不仅可以给结构体类型定义方法,还可以给非结构体类型定义方法,但是仍然需要满足前面类型定义与方法定义同包的要求。 比如我们想给int类型定义一个方法add。下面的定义是不行的,会报错cannot define new methods on non-local type int

package main

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

func main() {

}

因此,我们需要定义一个别名类型myInt,这样就可以在相同包内定义对应的方法:

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)
}

该程序输出:Sum is 15

Last updated