Go 语言指针:从基础到实践,一步到位

145 阅读9分钟

Go 语言指针:从基础到实践,一步到位

指针是 Go 语言中既简单又关键的存在,它连接变量与内存,让代码更高效、安全。无论你是刚入门 Go 的新手,还是想提升实践能力的老手,本文将带你从指针的基础知识出发,一路走到实际应用。只需一篇文章,你就能搞懂 Go 指针的精髓,轻松上手,学以致用!

本文从 Go 语言指针的定义讲起,介绍了 & 和 * 操作符的使用,逐步深入到指针在结构体、数组、切片中的妙用。通过代码示例和小测试,你将明白指针的安全设计、自动解引用特性,以及如何在函数和方法中高效操作数据。文章最后还提供了一个海龟移动的实践案例,让你立刻动手验证所学。一步到位,助你快速掌握 Go 指针的实战技巧!

指针

什么是指针

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

&和*符号

  • 变量会将它们的值存储在计算机的RAM里,存储位置就是该变量的内存地址。
  • & 表示地址操作符,通过&可以获得变量的内存地址。
package main

import "fmt"

func main() {
  answer := 42
  fmt.Println(&answer)
}
  • &操作符无法获得字符串/数值/布尔字面值的地址。

    • &42,&“hello”这些都会导致编译器报错
  • *操作符与&的作用相反,它用来解引用,提供内存地址指向的值。

package main

import "fmt"

func main() {
  answer := 42
  fmt.Println(&answer)
  
  address := &answer
  fmt.Println(*address)
}

注意

  • C语言中的内存地址可以通过例如address++这样的指针运算进行操作,但是在Go里面不允许这种不安全操作。

小测试

  1. 在例子中执行fmt.Println(*&answer)会打印出什么结果?
  2. 乘法运算和解引用都需要用到星号*,Go编译器将如何区分呢?

指针类型

  • 指针存储的是内存地址。
package main

import "fmt"

func main() {
  answer := 42
  
  address := &answer // *int
  
  fmt.Printf("address is a %T\n", address)
}
  • 指针类型和其它普通类型一样,出现在所有需要用到类型的地方,如变量声明、函数形参、返回值类型、结构体字段等。
package main

import "fmt"

func main() {
  canada := "Canada"
  
  var home *string
  fmt.Printf("home is a %T\n", home)
  
  home = &canada
  fmt.Printf("canada is a %v\n", *canada)
}
  • *放在类型前面表示声明指针类型
  • *放在变量前面表示解引用操作

小测试

  1. 你会使用什么代码来声明一个指向整数的名为address的变量?
  2. 你是如何区分 例子中声明指针变量和解引用指针这两个操作的?

指针就是用来指向的

package main

import "fmt"

func main() {
  var administrator *string
  
  scolese := "Christopher J. Scolese"
  administrator = &scolese
  fmt.Println(*administrator)
  
  bolden := "Charies F. Bolden"
  administrator = &bolden
  fmt.Println(*administrator)
  
  bolden = "Charles Frank Bolden Jr."
  fmt.Printf(*administrator)
  
  *administrator = "Maj. Gen. Charles Frank Bolden Jr."
  fmt.Println(bolden)
  
  major := administrator
  *major = "Major General Charles Frank Bolden Jr."
  fmt.Println(bolden)
  
  fmt.Println(administrator == major)
  
  lightfoot := "Robert M. Lightfoot Jr."
  administrator = &lightfoot
  fmt.Println(administrator == major)
  
  charles := *major
  *major = "Charles Bolden"
  fmt.Println(charles)
  fmt.Println(bolden)
  
  charlse = "Charles Bolden"
  fmt.Println(charles == bolden)
  fmt.Println(&charles == &bolden)
}
  • 两个指针变量持有相同的内存地址,那么它们就是相等。

小测试

  1. 例子中使用指针的好处是什么?
  2. 请说明语句major := administrator和Charles := *major的作用。

指向结构的指针

  • 与字符串和数值不一样,复合字面量的前面可以放置&。
package main

import "fmt"

func main() {
  type person struct {
    name, superpower string
    age int
  }
  
  timmy := &person {
    name: "Timothy",
    age: 10,
  }
  
  (*timmy).superpower = "flying"
  timmy.superpower = "flying"
  
  fmt.Printf("%+v\n", timmy)
}
  • 访问字段时,对结构体进行解引用并不是必须的。

小测试

  1. 以下哪些是&操作符的合法使用?

    1. A.放置在字符串字面值的前面,例如&“Tim”
    2. B.放置在整数字面值的前面,例如&10
    3. C.放置在复合字面值的前面,例如&person{name: “Tim”}
    4. D.以上全部都是
  2. 语句timmy.superpower和(*timmy).superpower有何区别?

指向数组的指针

  • 和结构体一样,可以把&放在数组的复合字面值前面来创建指向数组的指针。
package main

import "fmt"

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

小测试

  • 当superpower是一个指针或者数组时,有什么语句可以和*superpower具有同样的执行效果呢?

实现修改

  • Go语言的函数和方法都是按值传递参数的,这意味着函数总是操作于被传递参数的副本。
  • 当指针被传递到函数时,函数将接收传入的内存地址的副本。之后函数可以通过解引用内存地址来修改指针指向的值。

例子一个

package main

type person struct {
  name, superpower string
  age int
}

func birthday(p *person) {
  p.age++
}

func main() {
  
}

例子二

package main

type person struct {
  name, superpower string
  age int
}

func birthday(p *person) {
  p.age++
}

func main() {
  rebecca := person{
    name: "Rebecca",
    superpower: "imagination",
    age: 14,
  }
  
  birthday(&rebecca)
  
  fmt.Printf("%+v\n", rebecca)
}

小测试

  1. 对例子26.6来说,下列哪行代码会返回Timothy 11?

    1. A.birthday(&timmy
    2. )B.birthday(timmy)
    3. C.birthday(*timmy)
  2. 对于例子26.9和26.10来说,如果birthday(p person)函数不使用指针,那么Rebecca的岁数(age)将是多少?

指针接收者

  • 方法的接收者和方法的参数在处理指针方面是很相似的。
package main

type person struct {
  name string
  age int
}

func (p *person) birthday() {
  p.age++
}

func main() {
  terry := &person{
    name: "Terry",
    age: 15,
  }
  
  terry.birthday()
  
  fmt.Printf("%+v\n", terry)
  
  nathan := person{
    name: "Nathan",
    age: 17,
  }
  nathan.birthday()
  fmt.Printf("%+v\n", nathan)
}
  • Go语言在变量通过点标记法进行调用的时候,自动使用 & 取得变量的内存地址。

    • 所以不用写(&nathan).birthday()这种形式也可以正常运行。
package main

type person struct {
  name string
  age int
}

func (p *person) birthday() {
  p.age++
}

func main() {
  terry := &person{
    name: "Terry",
    age: 15,
  }
  
  terry.birthday()
  
  fmt.Printf("%+v\n", terry)
  
  nathan := person{
    name: "Nathan",
    age: 17,
  }
  nathan.birthday()
  (&nathan).birthday()
  fmt.Printf("%+v\n", nathan)
  
  const layout = "Mon, Jan 2, 2006"
  day := time.Now()
  tomorrow := day.Add(24 * time.Hour)
  
  fmt.Println(day.Format(layout))
  fmt.Println(tomorrow.Format(layout))
}

注意

  • 使用指针作为接收者的策略应该始终如一:
  • 如果一种类型的某些方法需要用到指针作为接收者,就应该为这种类型的所有方法都是用指针作为接收者。

小测试

  • 怎样才能判断time.Time类型所有的方法是否都没有使用指针作为接收者。

内部指针

  • Go语言提供了 内部指针 这种特性。
  • 它用于确定结构体中指定字段的内存地址。
package main

type stats struct {
  level int
  endurance, health int
}

func levelUp(s *stats) {
  s.level ++
  s.endurance = 42 + (14 * s.level)
  s.health = 5 * s.endurance
}

type character struct {
  name string
  stats stats
}

func main() {
  player := character(name: "Matthias")
  levelUp(&player.stats)
  
  fmt.Printf("%+v\n", player.stats)
}
  • &操作符不仅可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址。

小测试

  • 什么是内部指针?

修改数组

  • 函数通过指针对数组的元素进行修改。
package main

import "fmt"

func reset(board *[8][8]rune) {
  board[0][0] = 'r'
  // ...
}

func main() {
  var board [8][8]rune
  reset(&board)
  
  fmt.Printf("%c", board[0][0])
}

小测试

  • 什么情况下应该使用指向数组的指针?

隐式的指针

  • Go语言里一些内置的集合类型就在暗中使用指针。

  • map在被赋值或者被作为参数传递的时候不会被复制。

    • map就是一种隐式指针。
    • 这种写法就是多此一举:func demolish(planets *map[string]string)
  • map的键和值都可以是指针类型

  • 需要将指针指向map的情况并不多见

小测试

  • map是指针吗?

slice指向数组

  • 之前说过slice是指向数组的窗口,实际上slice在指向数组元素的时候也使用了指针。

  • 每个slice内部都会被表示为一个包含3个元素的结构,它们分别指向:

    • 数组的指针
    • slice的容量
    • slice的长度
  • 当slice被直接传递至函数或方法时,slice的内部指针就可以对底层数据进行修改。

  • 指向slice的显式指针的唯一作用就是修改slice本身:slice的长度、容量以及起始偏移量。

package main

import "fmt"

func reclassify(planets *[]string) {
  *planets = (*planets)[0:8]
}

func main() {
  planets := []string{
    "Mercury""Venus""Earth""Mars",
    "Jupiter""Saturn""Uranus""Neptune",
    "Pluto",
  }
  reclassify(&planets)
  
  fmt.Println(planets)
}

小测试

  • 如果函数和方法想要修改它们接收到的数据,那么它们应该使用指向哪两种数据类型的指针?

指针和接口

package main

import (
  "fmt"
  "strings"
)

type talker interface {
  talk() string
}

func shout(t talker) {
  louder := strings.ToUper(t.talk())
  fmt.Println(louder)
}

type martian struct {}

func (m martian) talk() string {
  return "nack nakc"
}

fun main() {
  shout(martian{})
  shout(&martian{})
}
  • 本例中,无论martian还是指向martian的指针,都可以满足talker接口。
  • 如果方法使用的是指针接收者,那么情况会有所不同。
package main

import (
  "fmt"
  "strings"
)

type talker interface {
  talk() string
}

func shout(t talker) {
  louder := strings.ToUper(t.talk())
  fmt.Println(louder)
}

type martian struct {}

func (m martian) talk() string {
  return "nack nakc"
}

type laser int

func (l *laser) talk() string {
  return strings.Repeat("pew "int(*l))
}

fun main() {
  pew := laser(2)
  shout(&pew)
  shout(pew) // 报错
}

小测试

  • 指针在什么情况下才能满足接口?

明智的使用指针

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

作业题

  • 编写一个可以让海龟上下左右移动的程序:

    • 程序中的海龟需要存储一个位置(x,y)
    • 正数坐标表示向下或向右
    • 通过使用方法对相应的变量实施自增和自减来实现移动
    • 请使用main函数测试这些方法并打印出海龟的最终位置

总结

从基础到实践,Go 语言的指针并不复杂。通过本文,你已经了解如何用 & 获取地址、* 解引用,还能在结构体和切片中灵活运用。相比 C 语言,Go 的指针更安全、更简洁,自动解引用和隐式指针让开发更省心。无论是修改数据还是定义方法,指针都能帮你高效解决问题。带着本文的示例和思路,去写代码试试吧,Go 指针的掌握,就在这一步!