lang

LangBook

9.方法

方法就是函数,但它是属于某一个类型。

1. 方法的定义

方法的语法为

func (t T) 方法名() {
    
}

你可以给任意类型定义方法

main.go
package main
 
import (
	"fmt"
)
 
type MyInt int
 
// 转换为string类型
func (i MyInt) Output() string { 
	return fmt.Sprintf("%v", i)
}

方法实际上是函数,上述方法等价于下列函数

main.go
package main
 
import (
	"fmt"
)
 
type MyInt int
 
// 转换为string类型
func Output(i MyInt) string { 
	return fmt.Sprintf("%v", i)
}

方法其实就是把函数参数移动到前面作为函数的接受者。

方法的定义必须要与类型的定义在同一个包里,例如你不能给int类型创建方法,因为int类型的定义不在main包中:

main.go
package main
 
import (
	"fmt"
)
 
// 编译期错误: cannot define new methods on non-local type int
func (i int) Output() string { 
	return fmt.Sprintf("%v", i)
}

但你可以给int起一个别名,给别名定义方法, 例如上一个例子。

2. 值接受者和指针接受者:

定义一个counter类型,它有一个值接收者的递增方法(increment):

main.go
package main
import "fmt"
 
// 给int起一个别名,新的类型counter
type counter int 
 
// 值接受者的方法,递增1
func (c counter) increment() { 
	c = c + 1
}
 
func main() {
	// 新建一个counter
	c := counter(0)
	// 调用递增的方法
	c.increment()
	// 打印c的值
	fmt.Println(c)
}

输出

0

为什么, 是0,而不是1呢?我们说过,方法其实就是函数

func (c counter) increment() {
	c = c + 1
}

等价于

func increment(c counter) {
	c = c + 1
}

Go中一切传递都是进行复制,索引increment函数中的c只是复制了传入的0,和main函数的c没有关系。

我们把increment方法改为指针接收者,结果就会大不一样:

main.go
package main
import "fmt"
 
// 给int起一个别名,新的类型counter
type counter int
 
// 指针接受者的方法,递增1
func (c *counter) increment() { 
	*c = *c + 1 // 指针解引用,*c 取出指向的变量
}
 
func main() {
	// 新建一个counter
	c := counter(0)
	// 调用递增的方法
	c.increment()
	// 打印c的值
	fmt.Println(c)
}

输出

1

为什么呢?

指针接受者的方法

// 指针接受者的方法,递增1
func (c *counter) increment() {
	*c = *c + 1
}

等价于

func increment(c *counter) {
	*c = *c + 1
}

传入的是指针地址,复制的是指针地址,main函数中的c的地址存的值发送了改变,所以打印1。

这就是指针接受者。当我们想改变c时,使用指针介绍者。或者当c内存占用很大时,我们使用指针接受者,减少函数传递时复制的开销,因为指针占用的内存永远是8个字节。

3. 指针语法糖

不知道你注意没有,counter调用指针接受者是这样的:

main.go
...
func main() {
	// 新建一个counter
	c := counter(0) 
	// 调用递增的方法
	c.increment() 
	// 打印c的值
	fmt.Println(c)
}
...

c 是counter类型,而不是*counter类型,为什么可以直接调用increment方法呢?

func (c *counter) increment() {
	*c = *c + 1
}

这是Go提供的指针语法糖, Go会自动帮我们取地址:

c.increment()

等价于

(&c).increment()

这能简化我们的操作。

同样,Go也会自动地解引用,考虑下列值接受者

main.go
package main
import "fmt"
 
// 给int起一个别名,新的类型counter
type counter int
 
// 值接受者
func (c counter) increment() { 
	c = c + 1
}
 
func main() {
	// 新建一个counter
	temp := counter(0) 
    c := &temp // c: *counter类型
	// 调用递增的方法
	c.increment() 
	// 打印c的值
	fmt.Println(*c) // 解引用取出c指向的变量
}

c是指针类型(*counter)的变量,increment是值接受者。

// 调用递增的方法
c.increment()

等价于

(*c).increment()

Go帮我们自动解引用。

聪明的你,上面的代码输出0还是1?

运行,输出

0

为什么呢?因为increment传入的是值类型counter,而不是指针类型(*counter)。

4. 结构体的方法

结构体(类型)就像一个人, 他有姓名,年龄等属性(字段), 也会唱,跳和rap等技能(方法)。

main.go
package main
 
import "fmt"
 
type person struct {
	name string
	age  int
}
 
// 唱
func (p person) sing() { 
	fmt.Printf("%s 在跳\n", p.name)
}
 
// 跳
func (p person) dance() { 
	fmt.Printf("%s 在唱\n", p.name)
}
 
// rap
func (p person) rap() { 
	fmt.Printf("%s 在rap\n", p.name)
}
 
func main() {
	p := person{
		name: "lang",
		age:  222,
	}
	p.sing() 
	p.dance()  
	p.rap() 
}

运行,输出

lang 在跳
lang 在唱
lang 在rap

5. 组合优于继承

如何更好地复用代码呢?其他语言通过面向对象来实现,Go特立独行,使用组合来实现类似面向对象的功能。

以上面的person结构体为例,如果我想在person的基础上创建新的结构体smartPerson,如何做呢?这时可以使用嵌套:

main.go
type smartPerson struct {
	person
	iQ int
}

嵌套结构体,这时不用使用字段名,直接把类型写入即可。smartPerson自己有一个iQ字段

新的结构体可以访问person的字段和方法

func main() {
	p := person{
		name: "lang",
		age:  222,
	}
 
	sm := smartPerson{
		person: p,
		iQ:     100,
	}
 
	fmt.Println(sm.age)  // 222
	fmt.Println(sm.name) // "lang"
	sm.sing()            // lang 在跳
	sm.dance()           // lang 在唱
}

smartPerson也可以实现和person相同的方法

func (sm smartPerson) sing() {
	sm.person.sing()
	fmt.Println("smartPerson 在唱")
}

这时调用sing() 结果是哪个呢?

func main() {
	p := person{
		name: "lang",
		age:  222,
	}
 
	sm := smartPerson{
		person: p,
		iQ:     100,
	}
 
	sm.sing() 
}

输出

lang 在跳
smartPerson 在唱

是自身的sing方法。通过结构体的嵌套,我们可以“重写”被嵌入的结构体的方法。

如果想调用被嵌套的sing方法,可以使用

sm.person.sing()

指明字段

运行,输出:

lang 在跳

好的。我们实现了类似“继承”的功能,但它是组合。组合优于继承!

完整代码

main.go
package main
 
import "fmt"
 
type person struct {
	name string
	age  int
}
 
// 唱
func (p person) sing() { 
	fmt.Printf("%s 在跳\n", p.name)
}
 
// 跳
func (p person) dance() { 
	fmt.Printf("%s 在唱\n", p.name)
}
 
type smartPerson struct {
	person
	iQ int
}
 
func (sm smartPerson) sing() {
	sm.person.sing()
	fmt.Println("smartPerson 在唱")
}
 
func main() {
	p := person{
		name: "lang",
		age:  222,
	}
 
	sm := smartPerson{
		person: p,
		iQ:     100,
	}
 
	// 调用字段
	fmt.Println(sm.age)  // 222
	fmt.Println(sm.name) // "lang"
	// 调用方法
	sm.sing()  // lang 在跳
	sm.dance() // lang 在唱
}

输出

222
lang
lang 在跳
smartPerson 在唱
lang 在唱

On this page