Kodewick
开始学 →
第 12 课,共 24 课目录

方法

第 12 课,共 24 课 · 第 3 部分 · 类型 · 约 15 分钟

方法是属于某个类型的函数。在 func 和名字之间写上 (r Rect),Area 就成了 Rect 的方法,用 box.Area() 调用:

prog.go
1package main2 3import "fmt"4 5type Rect struct {6	Width, Height float647}8 9func (r Rect) Area() float64 {10	return r.Width * r.Height11}12 13func main() {14	box := Rect{Width: 3, Height: 4}15	fmt.Println(box.Area())16}
输出
12

(r Rect) 叫接收者。调用 box.Area() 时,r 就是 box。

方法和函数

接收者其实就是一个写在前面的参数。下面的 area 函数和 Area 方法做的是同一件事,只是调用的写法不同:

prog.go
1package main2 3import "fmt"4 5type Rect struct {6	Width, Height float647}8 9func area(r Rect) float64 {10	return r.Width * r.Height11}12 13func (r Rect) Area() float64 {14	return r.Width * r.Height15}16 17func (r Rect) IsSquare() bool {18	return r.Width == r.Height19}20 21func main() {22	box := Rect{Width: 3, Height: 4}23	fmt.Println(area(box), box.Area())24	fmt.Println(box.IsSquare())25}
输出
12 12
false

方法的好处是,和这个类型有关的操作都挂在它身上,读代码的人一眼就知道去哪儿找。不同的类型可以有同名的方法:Circle 也可以有自己的 Area。

值接收者改不了原来的

接收者也是一份复制,和普通参数一样。所以下面的 Inc 加的是复制,clicks 一直是 0:

prog.go
1package main2 3import "fmt"4 5type Counter struct {6	N int7}8 9func (c Counter) Inc() {10	c.N++11}12 13func main() {14	var clicks Counter15	clicks.Inc()16	clicks.Inc()17	fmt.Println(clicks.N)18}
输出
0

点单步,走进 Inc:c 是一个新格子,c.N 变成 1,回到 main 后这个格子就没了。

指针接收者

把接收者写成指针 (c *Counter),方法改的就是原来的那个:

prog.go
1package main2 3import "fmt"4 5type Counter struct {6	N int7}8 9func (c *Counter) Inc() {10	c.N++11}12 13func main() {14	var clicks Counter15	clicks.Inc()16	clicks.Inc()17	fmt.Println(clicks.N)18}
输出
2

clicks 不是指针,可 clicks.Inc() 照样能用:Go 自动帮你写成 (&clicks).Inc()。再点单步,这回 c 是一个箭头,指着 main 里的 clicks。

先猜一猜

这段程序显示什么?

1package main2 3import "fmt"4 5type Account struct {6	Balance int7}8 9func (a Account) Deposit(n int) {10	a.Balance += n11}12 13func (a *Account) Withdraw(n int) {14	a.Balance -= n15}16 17func main() {18	acc := Account{Balance: 100}19	acc.Deposit(50)20	acc.Withdraw(30)21	fmt.Println(acc.Balance)22}

怎么选

  • 方法要改接收者:用指针接收者。
  • 结构体很大:用指针接收者,省得每次复制。
  • 一个类型的方法,习惯上要么都用指针接收者,要么都用值接收者,不混着用。

一个账户:存钱和取钱都要改余额,所以都用指针接收者。

prog.go
1package main2 3import "fmt"4 5type Account struct {6	Owner   string7	Balance int8}9 10func (a *Account) Deposit(n int) {11	a.Balance += n12}13 14func (a *Account) Withdraw(n int) bool {15	if n > a.Balance {16		return false17	}18	a.Balance -= n19	return true20}21 22func main() {23	acc := Account{Owner: "小林", Balance: 100}24	acc.Deposit(20)25	ok := acc.Withdraw(30)26	fmt.Println(ok, acc.Balance)27	ok = acc.Withdraw(500)28	fmt.Println(ok, acc.Balance)29}
输出
true 90
false 90

钱不够时 Withdraw 返回 false,余额不动。

命名类型上的方法

不只是结构体:用 type 起了名字的类型都能加方法。type Celsius float64 是一个新类型,底下是 float64;type Scores []int 底下是切片:

prog.go
1package main2 3import "fmt"4 5type Celsius float646 7func (c Celsius) Fahrenheit() float64 {8	return float64(c)*9/5 + 329}10 11type Scores []int12 13func (s Scores) Sum() int {14	total := 015	for _, v := range s {16		total += v17	}18	return total19}20 21func main() {22	t := Celsius(25)23	fmt.Println(t.Fahrenheit())24	s := Scores{90, 75, 88}25	fmt.Println(s.Sum())26}
输出
77
253

float64(c) 把 Celsius 换回普通的 float64。只能给自己这个包里定义的类型加方法,int、string 这些不行。

String 方法

给类型加一个 String() string 方法,fmt 显示这个类型的值时就会用它:

prog.go
1package main2 3import "fmt"4 5type Celsius float646 7func (c Celsius) String() string {8	return fmt.Sprintf("%.1f°C", float64(c))9}10 11func main() {12	t := Celsius(36.6)13	fmt.Println(t)14	fmt.Println([]Celsius{20, 25.5})15	fmt.Printf("现在 %v\n", t)16}
输出
36.6°C
[20.0°C 25.5°C]
现在 36.6°C

fmt.Sprintf 和 Printf 一样排格式,只是不显示,而是把结果作为字符串返回(%.1f 这些格式见第 10 课的表)。fmt 怎么知道要调用 String?下一课讲接口时就明白了。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

长方形的周长

写好 Rect 的 Perimeter 方法,让它返回周长(宽加高,再乘 2)。程序显示两个长方形的周长。

要显示
14
22
exercise1.go
1package main2 3import "fmt"4 5type Rect struct {6	Width, Height int7}8 9func (r Rect) Perimeter() int {10	return 011}12 13func main() {14	a := Rect{Width: 3, Height: 4}15	b := Rect{Width: 5, Height: 6}16	fmt.Println(a.Perimeter())17	fmt.Println(b.Perimeter())18}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:步数没加上

Add 应该把步数加到计步器上,可程序两次都显示 0。改好它,让它显示 2000 和 5500。

要显示
2000
5500
exercise2.go
1package main2 3import "fmt"4 5type StepCounter struct {6	Steps int7}8 9func (s StepCounter) Add(n int) {10	s.Steps += n11}12 13func main() {14	var today StepCounter15	today.Add(2000)16	fmt.Println(today.Steps)17	today.Add(3500)18	fmt.Println(today.Steps)19}
输出
点「运行」或按 Ctrl+Enter
练习 3

让价格自己带上货币符号

价格用「分」存成整数:Price(1250) 是 12.50 元。给 Price 加一个 String 方法,让 fmt.Println 显示 ¥ 和两位小数,比如 ¥12.50。

要显示
¥12.50
¥3.99
exercise3.go
1package main2 3import "fmt"4 5type Price int6 7func main() {8	fmt.Println(Price(1250))9	fmt.Println(Price(399))10}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 方法是带接收者的函数:func (r Rect) Area() float64,用 box.Area() 调用。
  • 值接收者拿到的是复制,改不到原来的;指针接收者 (c *Counter) 改的是原来的。
  • 对一个变量调用指针接收者的方法,Go 自动取地址:clicks.Inc()。
  • 要改接收者或结构体很大时用指针接收者;一个类型的方法不混着用。
  • 自己用 type 定义的类型都能加方法,比如 type Celsius float64。
  • 有 String() string 方法的类型,fmt 显示时用它。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。