Go 学习笔记(十七)Go 中的方法

本文原创地址:博客园骏马金龙Go 基础系列:Go 中的方法

Go 方法简介

Go 中的 struct 结构类似于面向对象中的类。面向对象中,除了成员变量还有方法。

Go 中也有方法,它是一种特殊的函数,定义于 struct 之上 (与 struct 关联、绑定),被称为 struct 的 receiver。

它的定义方式大致如下:

type mytype struct{}

func (recv mytype) my_method(para) return_type {}
func (recv *mytype) my_method(para) return_type {}

这表示my_method()函数是绑定在 mytype 这个 struct type 上的,是与之关联的,是独属于 mytype 的。所以,此函数称为 "方法"。所以,方法和字段一样,也是 struct 类型的一种属性。

其中方法名前面的(recv mytype)(recv *mytype)是方法的 receiver,具有了 receiver 的函数才能称之为方法,它将函数和 type 进行了关联,使得函数绑定到 type 上。至于 receiver 的类型是mytype还是*mytype,后面详细解释。

定义了属于 mytype 的方法之后,就可以直接通过 mytype 来调用这个方法:

mytype.my_method()

来个实际的例子,定义一个名为 rectangle 的 struct 类型,属性为长和宽,定义属于 rectangle 的求面积的方法 area()。

package main

import "fmt"

type rectangle struct {
	length float64
	width  float64
}

func (c *rectangle) area() float64 {
	return c.length * c.width
}

func main() {
	c := &rectangle{
		2.5,
		4.0,
	}
	fmt.Printf("%f\n",c.area())
}

方法的一些注意事项

1. 方法的 receiver type 并非一定要是 struct 类型,type 定义的类型别名、slice、map、channel、func 类型等都可以。但内置简单数据类型 (int、float 等) 不行,interface 类型不行

package main

import "fmt"

type myint int

func (i *myint) numadd(n int) int {
	return n + 1
}

func main() {
	n := new(myint)
	fmt.Println(n.numadd(4))
}

以 slice 为类型,定义属于它的方法:

package main

import "fmt"

type myslice []int

func (v myslice) sumOfSlice() int {
	sum := 0
	for _, value := range v {
		sum += value
	}
	return sum
}

func main() {
	s := myslice{11, 22, 33}
	fmt.Println(s.sumOfSlice())
}

2.struct 结合它的方法就等价于面向对象中的类。只不过 struct 可以和它的方法分开,并非一定要属于同一个文件,但必须属于同一个包。所以,没有办法直接在 int、float 等内置的简单类型上定义方法,真要为它们定义方法,可以像上面示例中一样使用 type 定义这些类型的别名,然后定义别名的方法

3. 方法有两种类型(T Type)(T *Type),它们之间有区别,后文解释。

4. 方法就是函数,所以 Go 中没有方法重载 (overload) 的说法,也就是说同一个类型中的所有方法名必须都唯一。但不同类型中的方法,可以重名 。例如:

func (a *mytype1) add() ret_type {}
func (a *mytype2) add() ret_type {}

5.type 定义类型的别名时,别名类型不会拥有原始类型的方法 。例如 mytype 上定义了方法 add(),mytype 的别名 new_type 不会有这个方法,除非自己重新定义。

6. 如果 receiver 是一个指针类型,则会自动解除引用 。例如,下面的 a 是指针,它会自动解除引用使得能直接调用属于 mytype1 实例的方法 add()。

func (a *mytype1) add() ret_type {}
a.add()

7.(T Type)(T *Type)的 T,其实就是面向对象语言中的 this 或 self,表示调用该实例的方法。如果愿意,自然可以使用 self 或 this,例如(self Type),但这是可以随意的。

8. 方法和 type 是分开的,意味着实例的行为 (behavior) 和数据存储 (field) 是分开的,但是它们通过 receiver 建立起关联关系

方法和函数的区别

其实方法本质上就是函数,但方法是关联了类型的,可以直接通过类型的实例去调用属于该实例的方法。

例如,有一个 type person,如果定义它的方法 setname()和定义通用的函数 setname2(),它们要实现相同的为 person 赋值名称时,参数不一样:

func (p *person) setname(name string) {
	p.name = name
}

func setname2(p *person,name string) {
	p.name = name
}

通过函数为 person 的 name 赋值,必须将 person 的实例作为函数的参数之一,而通过方法则无需声明这个额外的参数,因为方法是关联到 person 实例的。

值类型和指针类型的 receiver

假如有一个 person struct:

type person struct{
	name string
	age int
}

有两种类型的实例:

p1 := new(person)
p2 := person{}

p1 是指针类型的 person 实例,p2 是值类型的 person 实例。虽然 p1 是指针,但它也是实例。在需要访问或调用 person 实例属性时候,如果发现它是一个指针类型的变量,Go 会自动将其解除引用,所以p1.name在内部实际上是(*p1).name。同理,调用实例的方法时也一样,有需要的时候会自动解除引用。

除了实例有值类型和指针类型的区别,方法也有值类型的方法和指针类型的区别 ,也就是以下两种 receiver:

func (p person) setname(name string) { p.name = name }
func (p *person) setage(age int) { p.age = age }

setname()方法中是值类型的 receiver,setage() 方法中是指针类型的 receiver。它们是有区别的。

首先,setage() 方法的 p 是一个指针类型的 person 实例,所以方法体中的p.age实际上等价于(*p).age

再者,方法就是函数, Go 中所有需要传值的时候,都是按值传递的,也就是拷贝一个副本

setname() 中,除了参数name string需要拷贝,receiver 部分(p person)也会拷贝,而且它明确了要 拷贝的对象是值类型的实例 ,也就是拷贝完整的 person 数据结构。但实例有两种类型:值类型和指针类型。(p person)无视它们的类型,因为 receiver 严格规定 p 是一个值类型的实例。所以无论是指针类型的 p1 实例还是值类型的 p2 实例,都会拷贝整个实例对象。对于指针类型的实例 p1,前面说了,在需要的时候,Go 会自动解除引用,所以p1.setname()等价于(*p1).setname()

也就是说, 只要 receiver 是值类型的,无论是使用值类型的实例还是指针类型的实例,都是拷贝整个底层数据结构的,方法内部访问的和修改的都是实例的副本 。所以,如果有修改操作,不会影响外部原始实例。

setage() 中,receiver 部分(p *person)明确指定了要拷贝的对象是指针类型的实例,无论是指针类型的实例 p1 还是值类型的 p2,都是拷贝指针。所以p2.setage()等价于(&p2).setage()

也就是说, 只要 receiver 是指针类型的,无论是使用值类型的实例还是指针类型的实例,都是拷贝指针,方法内部访问的和修改的都是原始的实例数据结构 。所以,如果有修改操作,会影响外部原始实例。

那么选择值类型的 receiver 还是指针类型的 receiver?一般来说选择指针类型的 receiver。

下面的代码解释了上面的结论:

package main

import "fmt"

type person struct {
	name string
	age  int
}

func (p person) setname(name string) {
	p.name = name
}
func (p *person) setage(age int) {
	p.age = age
}

func (p *person) getname() string {
	return p.name
}
func (p *person) getage() int {
	return p.age
}

func main() {
	// 指针类型的实例
	p1 := new(person)
	p1.setname("longshuai1")
	p1.setage(21)
	fmt.Println(p1.getname()) // 输出""
	fmt.Println(p1.getage())  // 输出21

	// 值类型的实例
	p2 := person{}
	p2.setname("longshuai2")
	p2.setage(23)
	fmt.Println(p2.getname())  // 输出""
	fmt.Println(p2.getage())   // 输出23
}

上面分别创建了指针类型的实例 p1 和值类型的实例 p2,但无论是 p1 还是 p2,它们调用 setname()方法设置的 name 值都没有影响原始实例中的 name 值,所以 getname() 都输出空字符串,而它们调用 setage() 方法设置的 age 值都影响了原始实例中的 age 值。

嵌套 struct 中的方法

当内部 struct 嵌套进外部 struct 时,内部 struct 的方法也会被嵌套,也就是说外部 struct 拥有了内部 struct 的方法。

例如:

package main

import (
	"fmt"
)

type person struct{}

func (p *person) speak() {
	fmt.Println("speak in person")
}

// Admin exported
type Admin struct {
	person
	a int
}

func main() {
	a := new(Admin)
	// 直接调用内部struct的方法
	a.speak()
	// 间接调用内部stuct的方法
	a.person.speak()
}

当 person 被嵌套到 Admin 中后,Admin 就拥有了 person 中的属性,包括方法 speak()。所以,a.speak()a.person.speak()都是可行的。

如果 Admin 也有一个名为 speak()的方法,那么 Admin 的 speak() 方法将掩盖内部 struct 的 person 的 speak() 方法。所以a.speak()调用的将是属于 Admin 的 speak(),而a.preson.speak()将调用的是 person 的 speak()。

验证如下:

func (a *Admin) speak() {
	fmt.Println("speak in Admin")
}

func main() {
	a := new(Admin)
	// 调用外部struct的方法
	a.speak() 
	// 间接调用内部stuct的方法
	a.person.speak()
}

输出结果为:

speak in Admin
speak in person

嵌入方法的第二种方式

除了可以通过嵌套的方式获取内部 struct 的方法,还有一种方式可以获取另一个 struct 中的方法: 将另一个 struct 作为外部 struct 的一个命名字段

例如:

type person struct {
	name string
	age int
}
type Admin struct {
	people *person
	salary int
}

现在 Admin 除了自己的 salary 属性,还指向一个 person。这和 struct 嵌套不一样,struct 嵌套是直接外部包含内部,而这种组合方式是一个 struct 指向另一个 struct,从 Admin 可以追踪到其指向的 person。所以,它更像是链表。

例如,person 是 Admin type 中的一个字段,person 有方法 speak()。

package main

import (
	"fmt"
)

type person struct {
	name string
	age  int
}

type Admin struct {
	people *person
	salary int
}

func main() {
	// 构建Admin实例
	a := new(Admin)
	a.salary = 2300
	a.people = new(person)
	a.people.name = "longshuai"
	a.people.age = 23
	// 或a := &Admin{&person{"longshuai",23},2300}

	// 调用属于person的方法speak()
	a.people.speak()
}

func (p *person) speak() {
	fmt.Println("speak in person")
}

或者,定义一个属于 Admin 的方法,在此方法中应用 person 的方法:

func (a *Admin) sing(){
	a.people.speak()
}

然后只需调用a.sing()就可以隐藏 person 的方法。

多重继承

因为 Go 的 struct 支持嵌套多个其它匿名字段,所以支持 "多重继承"。这意味着外部 struct 可以从多个内部 struct 中获取属性、方法。

例如,照相手机 cameraPhone 是一个 struct,其内嵌套 Phone 和 Camera 两个 struct,那么 cameraPhone 就可以获取来自 Phone 的 call()方法进行拨号通话,获取来自 Camera() 的 takeAPic() 方法进行拍照。

面向对象的语言都强烈建议不要使用多重继承,甚至有些语言本就不支持多重继承。至于 Go 是否要使用 "多重继承",看需求了,没那么多限制。

重写 String() 方法

fmt 包中的 Println()、Print() 和 Printf() 的%v都会自动调用 String() 方法将待输出的内容进行转换。

可以在自己的 struct 上重写 String()方法,使得输出这个示例的时候,就会调用它自己的 String()。

例如,定义 person 的 String(),它将 person 中的 name 和 age 结合起来:

package main

import (
	"fmt"
	"strconv"
)

type person struct {
	name string
	age  int
}

func (p *person) String() string {
	return p.name + ": " + strconv.Itoa(p.age)
}

func main() {
	p := new(person)
	p.name = "longshuai"
	p.age = 23
	// 输出person的实例p,将调用String()
	fmt.Println(p)
}

上面将输出:

longshuai: 23

一定要注意,定义 struct 的 String()方法时,String() 方法里不要出现 fmt.Print()、fmt.Println 以及 fmt.Printf() 的%v,因为它们自身会调用 String(),会出现无限递归的问题。

上一篇 Go 学习笔记(十六)struct 和嵌套 struct
Go 学习笔记(目录)
下一篇 Go 学习笔记(十八)struct 的导出和暴露问题