指针
每个变量都放在内存里的某个位置,这个位置叫地址。指针是存着一个地址的值:它指向一个变量。通过指针,可以读、也可以改它指向的那个变量。
1package main2 3import "fmt"4 5func main() {6 price := 1007 p := &price8 fmt.Println(*p)9 *p = 8010 fmt.Println(price)11}
100 80
| 写法 | 意思 |
|---|---|
&price | price 的地址。 |
p := &price | p 指向 price。 |
*p | p 指向的那个变量:可以读,也可以赋值。 |
*p = 80 改的就是 price。点单步,看 p 画成一个箭头,指着 price。地址本身是一串数字,每次运行可能不一样,所以这门课不显示它。
指针的类型
指向 int 的指针,类型是 *int;指向 string 的是 *string。指针的零值是 nil:什么也不指。
1package main2 3import "fmt"4 5func main() {6 count := 37 var p *int8 fmt.Println(p == nil)9 p = &count10 fmt.Printf("%T %d\n", p, *p)11 q := new(int)12 *q = 713 fmt.Println(*q)14}
true *int 3 7
new(int) 新建一个值为零的 int,返回指向它的指针。它不常用:多数时候用 & 就够了。
函数拿到的是复制
第 5 课讲过:调用函数时,参数是一份复制。函数里改它,外面的变量不变:
1package main2 3import "fmt"4 5func addBonus(salary int) {6 salary += 5007 fmt.Println("函数里", salary)8}9 10func main() {11 pay := 300012 addBonus(pay)13 fmt.Println("函数外", pay)14}
函数里 3500 函数外 3000
传指针
想让函数改调用者的变量,就把变量的地址传进去。函数通过指针改,改的就是外面那个:
1package main2 3import "fmt"4 5func addBonus(salary *int) {6 *salary += 5007}8 9func main() {10 pay := 300011 addBonus(&pay)12 fmt.Println(pay)13}
3500
点单步,走进 addBonus:salary 是一个箭头,指着 main 里的 pay。和上一段比一比,那里的 salary 是一个装着 3000 的新格子。
这段程序显示什么?
1package main2 3import "fmt"4 5func double(n *int) {6 *n = *n * 27}8 9func main() {10 a := 511 b := a12 double(&b)13 fmt.Println(a, b)14}
b := a 把 5 复制给 b,两个是不同的变量。double 拿到的是 b 的地址,所以只有 b 变成 10。
结构体指针
指针最常指向结构体。通过结构体指针取字段,直接写 a.Balance 就行,Go 会自动帮你写成 (*a).Balance:
1package main2 3import "fmt"4 5type Account struct {6 Owner string7 Balance int8}9 10func deposit(a *Account, amount int) {11 a.Balance += amount12}13 14func main() {15 acc := Account{Owner: "小林", Balance: 100}16 deposit(&acc, 50)17 fmt.Println(acc.Balance)18 p := &Account{Owner: "小王"}19 deposit(p, 30)20 fmt.Println(p.Owner, p.Balance)21 fmt.Println(p)22}
150
小王 30
&{小王 30}
&Account{…} 新建一个结构体,同时拿到它的指针。显示结构体指针时,fmt 在前面加一个 &。
nil 指针
一个 nil 指针什么也不指。通过它读或写,程序会 panic:
1package main2 3import "fmt"4 5type Account struct {6 Owner string7 Balance int8}9 10func main() {11 var acc *Account12 fmt.Println(acc == nil)13 acc.Balance = 10014 fmt.Println("到不了这里")15}
true
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x?]
goroutine 1 [running]:
main.main(...)
/tmp/sandbox/prog.go:13 +0x1d
exit status 2发生了什么: 用了一个 nil 指针(它没有指向任何东西),比如没初始化的 *T 去取字段或调用方法。
invalid memory address or nil pointer dereference 的意思是:想通过一个 nil 指针去找变量,可它没有指向任何东西。点「看看是怎么出错的」,停在第 13 行。不确定一个指针是不是 nil 时,先用 if acc != nil 检查。
什么时候用指针
- 函数要改调用者的变量或结构体时。
- 结构体很大,不想每次传参都复制一份时。
- 想用
nil表示「还没有」时。
切片和 map 不用传指针也能改到里面的元素:它们本来就和调用者共用底层的数据(第 7、8 课)。
1package main2 3import "fmt"4 5func discount(prices []int) {6 for i := range prices {7 prices[i] = prices[i] * 9 / 108 }9}10 11func main() {12 prices := []int{100, 50}13 discount(prices)14 fmt.Println(prices)15}
[90 45]
不过,函数里 append 让切片变长,外面是看不到的:那种情况让函数把新切片返回。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
用指针交换两个数
✓写好 swap,让它交换两个指针指向的变量的值。程序交换了两组数。
2 1 70 30
1package main2 3import "fmt"4 5func swap(a, b *int) {6}7 8func main() {9 x, y := 1, 210 swap(&x, &y)11 fmt.Println(x, y)12 m, n := 30, 7013 swap(&m, &n)14 fmt.Println(m, n)15}
给商品打折
✓写好 applyDiscount,通过指针把商品的价格降低 percent%(percent 是 10 就是打九折)。程序给两件商品打了折。
台灯 180 水杯 60
1package main2 3import "fmt"4 5type Product struct {6 Name string7 Price int8}9 10func applyDiscount(p *Product, percent int) {11}12 13func main() {14 lamp := Product{Name: "台灯", Price: 200}15 mug := Product{Name: "水杯", Price: 75}16 applyDiscount(&lamp, 10)17 applyDiscount(&mug, 20)18 fmt.Println(lamp.Name, lamp.Price)19 fmt.Println(mug.Name, mug.Price)20}
修好它:没有账户的指针
✓这段程序会 panic。让 acc 指向一个属于「小林」、余额为 0 的账户,程序就会显示存了两次钱以后的余额。
小林 100 小林 150
1package main2 3import "fmt"4 5type Account struct {6 Owner string7 Balance int8}9 10func deposit(a *Account, amount int) {11 a.Balance += amount12}13 14func main() {15 var acc *Account16 deposit(acc, 100)17 fmt.Println(acc.Owner, acc.Balance)18 deposit(acc, 50)19 fmt.Println(acc.Owner, acc.Balance)20}
这一课学会了
&x是x的地址;p := &x让p指向x,p的类型是*int。*p是p指向的变量,可以读也可以改;指针的零值是nil。- 函数参数是复制;要改调用者的变量,传它的地址
f(&x),函数里用*p改。 - 结构体指针取字段直接写
p.Balance;&Account{…}新建结构体并拿到指针。 - 通过
nil指针读写会 panic:nil pointer dereference。 - 切片和 map 不用指针也能改到元素。