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里面不允许这种不安全操作。
小测试
- 在例子中执行
fmt.Println(*&answer)会打印出什么结果? - 乘法运算和解引用都需要用到星号
*,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)
}
- 将
*放在类型前面表示声明指针类型 - 将
*放在变量前面表示解引用操作
小测试
- 你会使用什么代码来声明一个指向整数的名为address的变量?
- 你是如何区分 例子中声明指针变量和解引用指针这两个操作的?
指针就是用来指向的
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)
}
- 两个指针变量持有相同的内存地址,那么它们就是相等。
小测试
- 例子中使用指针的好处是什么?
- 请说明语句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)
}
- 访问字段时,对结构体进行解引用并不是必须的。
小测试
-
以下哪些是&操作符的合法使用?
- A.放置在字符串字面值的前面,例如&“Tim”
- B.放置在整数字面值的前面,例如&10
- C.放置在复合字面值的前面,例如&person{name: “Tim”}
- D.以上全部都是
-
语句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)
}
小测试
-
对例子26.6来说,下列哪行代码会返回Timothy 11?
- A.birthday(&timmy
- )B.birthday(timmy)
- C.birthday(*timmy)
-
对于例子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 指针的掌握,就在这一步!