什么是指针

  • 指针是指向另一个变量地址的变量。
  • Go 语言的指针同时也强调安全性,不会出现迷途指针(dangling pointers)

指针 - 图1

& 和 * 符号

  • 变量会将它们的值存储在计算机的 RAM 里,存储位置就是该变量的内存地址。
  • & 表示地址操作符,通过 & 可以获得变量的内存地址。
  • & 操作符无法获得字符串/数值/布尔字面值的地址。
    • &42,&“hello”这些都会导致编译器报错
  • 操作符与 & 的作用相反,它用来解引用,提供内存地址指向的值。

指针 - 图2

  • C 语言中的内存地址可以通过例如 address++ 这样的指针运算进行操作,但是在 Go 里面不允许这种不安全操作。
  1. package main
  2. import "fmt"
  3. func main() {
  4. answer := 42
  5. fmt.Println(&answer)
  6. address := &answer
  7. fmt.Println(*address)
  8. }

指针类型

  • 指针存储的是内存地址。
  1. package main
  2. import "fmt"
  3. func main() {
  4. answer := 42
  5. address := &answer
  6. fmt.Printf("address is a %T\n", address)
  7. }
  • 指针类型和其它普通类型一样,出现在所有需要用到类型的地方,如变量声明、函数形参、返回值类型、结构体字段等。
  1. package main
  2. import "fmt"
  3. func main() {
  4. canada := "Canada"
  5. var home *string
  6. fmt.Printf("home is a %T\n", home)
  7. home = &canada
  8. fmt.Println(*home)
  9. }
  • 将 * 放在类型前面表示声明指针类型
  • 将 * 放在变量前面表示解引用操作

指针就是用来指向的

  1. package main
  2. import "fmt"
  3. func main() {
  4. var administrator *string
  5. scolese := "Christopher J. Scolese"
  6. administrator = &scolese
  7. fmt.Println(*administrator)
  8. bolden := "Charles F. Bolden"
  9. administrator = &bolden
  10. fmt.Println(*administrator)
  11. bolden = "Charles Frank Bolden Jr."
  12. fmt.Println(*administrator)
  13. *administrator = "Maj. Gen. Charles Frank Bolden Jr."
  14. fmt.Println(bolden)
  15. major := administrator
  16. *major = "Major General Charles Frank Bolden Jr."
  17. fmt.Println(bolden)
  18. fmt.Println(administrator == major)
  19. lightfoot := "Robert M. Lightfoot Jr."
  20. administrator = &lightfoot
  21. fmt.Println(administrator == major)
  22. charles := *major
  23. *major = "Charles Bolden"
  24. fmt.Println(charles)
  25. fmt.Println(bolden)
  26. charles = "Charles Bolden"
  27. fmt.Println(charles == bolden)
  28. fmt.Println(&charles == &bolden)
  29. }
  • 两个指针变量持有相同的内存地址,那么它们就是相等。

指针 - 图3

指向结构的指针

  • 与字符串和数值不一样,复合字面量的前面可以放置 &。
  • 访问字段时,对结构体进行解引用并不是必须的。
  1. package main
  2. import "fmt"
  3. func main() {
  4. type person struct {
  5. name, superpower string
  6. age int
  7. }
  8. timmy := &person{
  9. name: "Timothy",
  10. age: 10,
  11. }
  12. timmy.superpower = "flying"
  13. fmt.Printf("%+v\n", timmy)
  14. }

指向数组的指针

  • 和结构体一样,可以把 & 放在数组的复合字面值前面来创建指向数组的指针。
  1. package main
  2. import "fmt"
  3. func main() {
  4. superpowers := &[3]string{"flight", "invisibility", "super strength"}
  5. fmt.Println(superpowers[0])
  6. fmt.Println(superpowers[1:2])
  7. }
  • 数组在执行索引或切片操作时会自动解引用。没有必要写 (*superpower)[0] 这种形式。
  • 与 C 语言不一样,Go 里面数组和指针式两种完全独立的类型。
  • slice 和 map 的复合字面值前面也可以放置 & 操作符,但是 Go 并没有为它们提供自动解引用的功能。

实现修改

  • Go 语言的函数和方法都是按值传递参数的,这意味着函数总是操作于被传递参数的副本。
  • 当指针被传递到函数时,函数将接收传入的内存地址的副本。之后函数可以通过解引用内存地址来修改指针指向的值。
  1. package main
  2. import "fmt"
  3. type person struct {
  4. name, superpower string
  5. age int
  6. }
  7. func birthday(p *person) {
  8. p.age++
  9. }
  10. func main() {
  11. rebecca := person{
  12. name: "Rebecca",
  13. superpower: "imagination",
  14. age: 14,
  15. }
  16. birthday(&rebecca)
  17. fmt.Printf("%+v\n", rebecca)
  18. }

指针接收者

  • 方法的接收者和方法的参数在处理指针方面是很相似的。
  1. package main
  2. import "fmt"
  3. type person struct {
  4. name string
  5. age int
  6. }
  7. func (p *person) birthday() {
  8. p.age++
  9. }
  10. func main() {
  11. terry := &person{
  12. name: "Terry",
  13. age: 15,
  14. }
  15. terry.birthday()
  16. fmt.Printf("%+v\n", terry)
  17. nathan := person{
  18. name: "Nathan",
  19. age: 17,
  20. }
  21. nathan.birthday()
  22. fmt.Printf("%+v\n", nathan)
  23. }
  • Go 语言在变量通过点标记法进行调用的时候,自动使用 & 取得变量的内存地址。
    • 所以不用写 (&nathan).birthday() 这种形式也可以正常运行。
  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. const layout = "Mon, Jan 2, 2006"
  8. day := time.Now()
  9. tomorrow := day.Add(24 * time.Hour)
  10. fmt.Println(day.Format(layout))
  11. fmt.Println(tomorrow.Format(layout))
  12. }
  • 使用指针作为接收者的策略应该始终如一:
  • 如果一种类型的某些方法需要用到指针作为接收者,就应该为这种类型的所有方法都是用指针作为接收者。

内部指针

  • Go 语言提供了 内部指针 这种特性。
  • 它用于确定结构体中指定字段的内存地址。
  • & 操作符不仅可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址。
  1. package main
  2. import "fmt"
  3. type stats struct {
  4. level int
  5. endurance, health int
  6. }
  7. func levelUp(s *stats) {
  8. s.level++
  9. s.endurance = 42 + (14 * s.level)
  10. s.health = 5 * s.endurance
  11. }
  12. func main() {
  13. type character struct {
  14. name string
  15. stats stats
  16. }
  17. player := character{name: "Matthias"}
  18. levelUp(&player.stats)
  19. fmt.Printf("%+v\n", player.stats)
  20. }

修改数组

  • 函数通过指针对数组的元素进行修改。
  1. package main
  2. import "fmt"
  3. func reset(board *[8][8]rune) {
  4. board[0][0] = 'r'
  5. // ...
  6. }
  7. func main() {
  8. var board [8][8]rune
  9. reset(&board)
  10. fmt.Printf("%c", board[0][0])
  11. }

隐式的指针

  • Go 语言里一些内置的集合类型就在暗中使用指针。
  • map 在被赋值或者被作为参数传递的时候不会被复制。
    • map 就是一种隐式指针。
    • 这种写法就是多此一举:func demolish(planets *map[string]string)
  • map 的键和值都可以是指针类型
  • 需要将指针指向 map 的情况并不多见

slice 指向数组

  • 之前说过 slice 是指向数组的窗口,实际上 slice 在指向数组元素的时候也使用了指针。
  • 每个 slice 内部都会被表示为一个包含 3 个元素的结构,它们分别指向:
    • 数组的指针
    • slice 的容量
    • slice 的长度
  • 当 slice 被直接传递至函数或方法时,slice 的内部指针就可以对底层数据进行修改。
  • 指向 slice 的显式指针的唯一作用就是修改 slice 本身:slice 的长度、容量以及起始偏移量。
  1. package main
  2. import "fmt"
  3. func reclassify(planets *[]string) {
  4. *planets = (*planets)[0:8]
  5. }
  6. func main() {
  7. planets := []string{
  8. "Mercury", "Venus", "Earth", "Mars",
  9. "Jupiter", "Saturn", "Uranus", "Neptune",
  10. "Pluto",
  11. }
  12. reclassify(&planets)
  13. fmt.Println(planets)
  14. }

指针和接口

  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. )
  6. type talker interface {
  7. talk() string
  8. }
  9. func shout(t talker) {
  10. louder := strings.ToUpper(t.talk())
  11. fmt.Println(louder)
  12. }
  13. type martian struct{}
  14. func (m martian) talk() string {
  15. return "nack nack"
  16. }
  17. func main() {
  18. shout(martian{})
  19. shout(&martian{})
  20. }
  • 本例中,无论 martian 还是指向 martian 的指针,都可以满足 talker 接口。
  • 如果方法使用的是指针接收者,那么情况会有所不同。
  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. )
  6. type talker interface {
  7. talk() string
  8. }
  9. func shout(t talker) {
  10. louder := strings.ToUpper(t.talk())
  11. fmt.Println(louder)
  12. }
  13. type laser int
  14. func (l *laser) talk() string {
  15. return strings.Repeat("pew ", int(*l))
  16. }
  17. func main() {
  18. pew := laser(2)
  19. shout(&pew)
  20. }

明智的使用指针

  • 应合理使用指针,不要过度使用指针。

作业题

  • 编写一个可以让海龟上下左右移动的程序:
    • 程序中的海龟需要存储一个位置(x,y)
    • 正数坐标表示向下或向右
    • 通过使用方法对相应的变量实施自增和自减来实现移动
    • 请使用 main 函数测试这些方法并打印出海龟的最终位置
  1. package main
  2. import "fmt"
  3. type turtle struct {
  4. x, y int
  5. }
  6. func (t *turtle) up() {
  7. t.y--
  8. }
  9. func (t *turtle) down() {
  10. t.y++
  11. }
  12. func (t *turtle) left() {
  13. t.x--
  14. }
  15. func (t *turtle) right() {
  16. t.x++
  17. }
  18. func main() {
  19. var t turtle
  20. t.up()
  21. t.up()
  22. t.left()
  23. t.left()
  24. fmt.Println(t)
  25. t.down()
  26. t.down()
  27. t.right()
  28. t.right()
  29. fmt.Println(t)
  30. }