方法
方法是属于某个类型的函数。在 func 和名字之间写上 (r Rect),Area 就成了 Rect 的方法,用 box.Area() 调用:
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 方法做的是同一件事,只是调用的写法不同:
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:
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),方法改的就是原来的那个:
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}
Deposit 是值接收者,加的是复制,acc 没变。Withdraw 是指针接收者,真的减掉了 30。所以是 100 - 30 = 70。
怎么选
- 方法要改接收者:用指针接收者。
- 结构体很大:用指针接收者,省得每次复制。
- 一个类型的方法,习惯上要么都用指针接收者,要么都用值接收者,不混着用。
一个账户:存钱和取钱都要改余额,所以都用指针接收者。
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 底下是切片:
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 显示这个类型的值时就会用它:
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?下一课讲接口时就明白了。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
长方形的周长
✓写好 Rect 的 Perimeter 方法,让它返回周长(宽加高,再乘 2)。程序显示两个长方形的周长。
14 22
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}
修好它:步数没加上
✓Add 应该把步数加到计步器上,可程序两次都显示 0。改好它,让它显示 2000 和 5500。
2000 5500
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}
让价格自己带上货币符号
✓价格用「分」存成整数:Price(1250) 是 12.50 元。给 Price 加一个 String 方法,让 fmt.Println 显示 ¥ 和两位小数,比如 ¥12.50。
¥12.50 ¥3.99
1package main2 3import "fmt"4 5type Price int6 7func main() {8 fmt.Println(Price(1250))9 fmt.Println(Price(399))10}
这一课学会了
- 方法是带接收者的函数:
func (r Rect) Area() float64,用box.Area()调用。 - 值接收者拿到的是复制,改不到原来的;指针接收者
(c *Counter)改的是原来的。 - 对一个变量调用指针接收者的方法,Go 自动取地址:
clicks.Inc()。 - 要改接收者或结构体很大时用指针接收者;一个类型的方法不混着用。
- 自己用
type定义的类型都能加方法,比如type Celsius float64。 - 有
String() string方法的类型,fmt显示时用它。