方法
1 什么是方法?
方法其实就是一个函数,在 func
这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
。在python中就是绑定给对象的方法,哪个对象来调用,就把自己self传过去。
下面就是创建一个方法的语法。
func (t Type) methodName(parameter list) {
}
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
其中,
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
举个例子:
package main
import (
"fmt"
)
//Person 结构体
type Person struct {
name string
age int8
}
//普通函数dream
func dream() {
fmt.Println("我的梦想是学好Go语言!")
}
//Dream函数指定接收者后就是方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name) // p类似python中的self,谁调用Dream方法,就把谁传进来(p就是谁)
}
func main() {
//dream() 调用普通函数
var p1 = Person{ //实例化得到p1, p1就可以调用Dream方法
name:"egon",
age:18,}
p1.Dream()
}
上面程序输出为:egon的梦想是学好Go语言!
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
函数谁都可以调用。方法是某个具体的类型才能调用的函数。
2 为什么我们已经有函数了还需要方法呢?
我们把上面的程序重写成只使用函数:
package main
import (
"fmt"
)
type Person struct {
name string
age int
}
func Dream(p Person) {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := Person{name: "egon", age: 18}
Dream(p1)
}
在上面的程序中,Dream 方法被转化为一个函数,Person
结构体被当做参数传递给它。这个程序也产生完全相同的输出:egon的梦想是学好Go语言!
。
既然我们可以使用函数写出相同的程序,那么为什么我们需要方法?这有着几个原因,让我们一个个的看看。
Go 不是纯粹的面向对象编程语言
,而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。- 相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。假设我们有一个
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
上面方法的属性被使用在接口中。我们将在接下来的教程中讨论这个问题。
3 指针接收者与值接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this
或者self
。
package main
import (
"fmt"
)
type Person struct {
name string
age int
}
func (p *Person) Dream() {
p.age = 99
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := Person{name: "egon", age: 18}
(&p1).Dream()
fmt.Println(p1.age)
}
上面程序中,Dream方法的接收者是一个指针; 实例化得到的结构体p1,需要用指针调用Dream方法,使用&p1
语法,Go语言支持直接使用p1.Dream
上面程序输出为:
egon的梦想是学好Go语言!
99
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
package main
import (
"fmt"
)
type Person struct {
name string
age int
}
func (p Person) Dream() {
p.age = 99
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := Person{name: "egon", age: 18}
p1.Dream()
fmt.Println(p1.age)
}
上面程序输出为:
egon的梦想是学好Go语言!
18
什么时候应该使用指针类型接收者?
- 对方法内部的接收者所做的改变应该对调用者可见时。
- 当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收者需要拷贝整个结构体,这是很昂贵的。在这种情况下使用指针接收者,结构体不会被拷贝,只会传递一个指针到方法内部使用。
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
4 匿名字段的方法
属于结构体的匿名字段的方法可以被直接调用,就好像这些方法是属于定义了匿名字段的结构体一样。
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() //访问 address 结构体的 fullAddress 方法
}
在上面程序的第 32 行,我们通过使用 p.fullAddress()
来访问 address
结构体的 fullAddress()
方法。明确的调用 p.address.fullAddress()
是没有必要的,p.fullAddress()
访问提升字段的方法,就像它是在结构体 p
中声明的一样。该程序输出:
Full address: Los Angeles, California
还是以上面的程序为例,如果person
结构体内的方法,和address
结构体内的方法重名,又是如何取值呢?
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)
}
func (p person) fullAddress() {
p.city = "成都" //字段提升
p.state = "四川"
fmt.Printf("Full address: %s, %s", p.city, p.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() //访问 person 结构体的 fullAddress 方法
fmt.Println()
p.address.fullAddress() //访问 address 结构体的 fullAddress 方法
}
子类和父类方法重名,优先使用自己的,类似于面向对象的派生,子类重写了父类的方法。上面的程序输出为:
Full address: 成都, 四川
Full address: Los Angeles, California
5 任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
前提条件:不能给别的包定义的类型添加方法,也就是说,方法的接收者类型定义和方法的定义应该在同一个包中。比如,int
是Go
语言内置包定义的类型,所以我们不能为int
添加方法。
但是我们基于内置的int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
package main
import "fmt"
type myInt int
func (m myInt) add(b myInt) myInt { //接收者是myInt类型, 方法的参数也是myInt类型, 返回值也是myInt类型
return m + b
}
func main() {
num1 := myInt(5) //实例化得到myInt类型的接收者(对象),并初始化值
num2 := myInt(10)
sum := num1.add(num2) //num1调用add方法,把自己传进去
fmt.Println("Sum is", sum)
}
在上面程序的第5行,我们为 int
创建了一个类型别名 myInt
。在第7行,我们定义了一个以 myInt
为接收者的的方法 add
。
该程序将会打印出 Sum is 15
。
6 在方法中使用值接收者 与 在函数中使用值参数
当一个函数有一个值参数,它只能接受一个值参数。只能传值,不能传指针。
当一个方法有一个值接收者,它可以接受值接收者和指针接收者。可以用值来调,也可以用指针来调。
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()//通过指针调用值接收器
}
第 12 行的函数 func area(r rectangle)
接受一个值参数,方法 func (r rectangle) area()
接受一个值接收者。
在第 25 行,我们通过值参数 area(r)
来调用 area 这个函数,这是合法的。同样,我们使用值接收器来调用 area 方法 r.area()
,这也是合法的。
在第 28 行,我们创建了一个指向 r
的指针 p
。如果我们试图把这个指针传递到只能接受一个值参数的函数 area,编译器将会报错。所以我把代码的第 33 行注释了。如果你把这行的代码注释去掉,编译器将会抛出错误 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()
。该程序将会输出:
Area Function result: 50
Area Method result: 50
Area Method result: 50
7 在方法中使用指针接收者 与 在函数中使用指针参数
和值参数相类似,函数使用指针参数只接受指针,而使用指针接收器的方法可以使用值接收器和指针接收器。
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()//使用值来调用指针接收器
}
在上面程序的第 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
。这是被允许的,为了方便Go语言把代码 r.perimeter()
解释为 (&r).perimeter()
。该程序输出:
perimeter function output: 30
perimeter method output: 30
perimeter method output: 30