Go教程第十二篇: 方法
方法
本文我们讲述方法。
简介
在func关键字和方法名之间存在接收者类型的那些函数,我们称之为方法。接收者可以是结构体类型,也可以是非结构体类型。
方法声明的语法,如下:
func (t Type) methodName(parameter list) {
}
上面这个代码片,就是声明了一个名为methodName的方法,它的接收者类型是Type。t被称为接收者,在方法内部可以访问接收者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
}
在上面的程序中,我们在Employee结构体上创建了一个displaySalary方法。displaySalary()方法访问了接收者e,同时使用接收者e打印出了name、currency、salary。我们使用了语法 emp1.displaySalary()语法调用了此方法。程序的输出为:Salary of Sam Adolf is $5000。
方法 VS 函数
上面的程序,也可以用函数重写,完全可以不用方法。
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)
}
上面的程序,把displaySalary方法改成了一个函数,这个函数接收了一个Employee结构体作为参数,同时这个函数也和displaySalary产生了相同的输出。Salary of Sam Adolf is $5000。
那么,既然我们可以使用函数来完成和方法完全相同的工作,我们为什么还要方法呢?这样做有很多理由,我们来一个个看看这些理由是什么。
. Go并不是一个纯粹的面向对象的语言,它不支持类。因此,方法是实现和类相似的一种方式。我们可以为类型创建一组方法。
在上面的程序中,和Employee类相关的所有行为都可以用Employee做为接收者类型来创建方法。例如,我们可以创建方法: calculatePension、calculateLeaves等等。
. 具有相同名称的方法可以定义在不同的类型上。我们假定有俩个结构体:Square和Circle。那么我们可以在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)
fmt.Printf("\nEmployee age after change: %d", e.age)
}
在上面的程序中,changeName方法有一个值接收者(e Employee)而changeAge方法有一个指针接收者(e *Employee)。在changeName中对Employee结构体的name字段做出的修改对其调用者来说是不可见的。因此,在方法e.changeName("Michael Andrew")调用的前后打印的name值都是相同的。
而changeAge方法有一个指针接收者(e *Employee),在函数调用(&e).changAge(51)之后,对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)来调用此方法,但是不必要这样调用,Go为我们提供了另一个选择,我们可以使用e.changAge(51)来调用。e.changeAge(51)将要被解释为(&e).changeAge(51)。
我们重写上面的程序,使用e.changeAge(51)来替换(&e).changeAge(51),它打印的是相同的输出。
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)
fmt.Printf("\nEmployee age after change: %d", e.age)
}
什么时候使用指针接收,什么时候使用值接收
通常来说,当对接收者做出的修改需要对调用者可见时,就使用指针接收者。此外,在某些复制数据结构体代价比较大时,也可以使用指针接收者。
设想一下,如果某个结构体有许多字段,那么使用此结构体作为值接收者的话,就需要复制全部的结构体,而这样复制的操作是非常昂贵的。
在这种情况下,如果使用指针接收者的话,就不需要复制结构体,只需要一个指针即可。在其他场景中,可以使用值接收者。
匿名结构体字段的方法
匿名结构体字段的方法可以被调用,就好像它们属于这个结构体一样。
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
方法的值接收者 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
}
函数func area(r rectangle)接收一个值参,同时方法func (r rectangle) area()接收了一个值接收者。当我们用area(r)调用这个函数时,它可以正常运行,相似地,我们使用值接收者调用方法r.area()时,它也可以正常运行。
在程序中,我们还创建了一个指向r的指针。如果把这个指针传递给函数area的话,编译器会报错compilation error, cannot use p (type *rectangle) as type rectangle in argument to area。
现在我们来看最棘手的部分,程序中代码p.area()使用指针接收者p调用了方法area,而方法area是只接受值接收者的。但是这样却没问题,原因是p.area()会被Go解释为(*p).area()
。
程序将输出如下:
Area Function result: 50
Area Method result: 50
Area Method result: 50
方法的指针接收者 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
}
在上面的程序中,定义了一个函数perimeter,它接收指针参数。定义了一个方法perimeter,它接收指针接收者。
当我们使用传递值参给函数perimeter时,编译器是不允许的。因为指针参数的函数不能接收值参。
而当我们用一个值接收者r调用方法perimeter的时候,却没问题。代码r.perimeter()会被编译器解释为(&r).perimeter()。
程序的输出如下:
perimeter function output: 30
perimeter method output: 30
perimeter method output: 30
非结构体接收者的函数
到目前为止,我们都是在结构体类型上定义方法。但其实,在非结构体类型上,也能定义方法。但是有个限制是,在类型上定义方法时,接收者的定义和方法定义必须在同一个包下。我们现在都是在main包下面定义的,因此,他们都没问题。
package main
func (a int) add(b int) {
}
func main() {
}
在上面的程序中,我们试图在内置的数据类型int上面添加一个add方法。这是不允许的,因为add()方法的定义和int类型的定义不在同一个包下面。程序会抛出一个编译错误:cannot define new methods on non-local type int。
那么怎么解决这个问题呢?我们可以为内置类型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)
}
在上面的程序中,我们为int创建了一个类型别名myInt,然后我们定义了一个以myInt作为接收者的add方法。
程序将输出:Sum is 15 。
上述这些就是Go中的方法,祝您愉快!
本篇系翻译之作,原文地址