10 - Go 指针:从入门到避坑

文章目录


前言

在很多编程语言中,"指针"是一个既强大又容易让人困惑的概念。而在 Go 语言中,指针被设计得相对简单,但依然非常重要。

👉 本文将带你了解:

  • 指针的本质
  • Go 中如何使用指针
  • 指针与函数、结构体的关系
  • 常见坑位解析(面试高频🔥)

什么是指针?

指针的本质

👉 指针:存储变量内存地址的变量

换句话说:指针是一个变量,它存储的是另一个变量在内存中的地址

text 复制代码
变量 = 存数据
指针 = 存变量的地址

示例理解

go 复制代码
package main

import "fmt"

func main() {
	x := 10
	p := &x
	
	fmt.Println("x 的值:", x)
	fmt.Println("x 的地址:", &x)
	fmt.Println("p 的值:", p)
}

👉 输出:

bash 复制代码
x 的值: 10
x 的地址: 0xc000010120
p 的值: 0xc000010120

📌 说明:

  • &x:获取变量地址
  • p:就是指针变量,保存地址

指针的基本操作

声明指针

go 复制代码
var p *int

📌 含义:

  • p 是一个指针
  • 指向 int 类型

解引用(取值)

go 复制代码
package main

import "fmt"

func main() {
	x := 10
	p := &x

	fmt.Println(*p) // 取值
}

👉 输出:

bash 复制代码
10

📌 说明:

  • *p:访问指针指向的值

修改变量值

go 复制代码
package main

import "fmt"

func main() {
	x := 10
	p := &x

	fmt.Println("x 的值:", x)
	fmt.Println("x 的地址:", &x)
	fmt.Println("p 的值:", p)
	fmt.Println("p 内存的解值:", *p)
	*p = 20
	fmt.Println("x 的值:", x)
	fmt.Println("x 的地址:", &x)
	fmt.Println("p 的值:", p)
	fmt.Println("p 内存的解值:", *p)
}

输出:

bath 复制代码
x 的值: 10
x 的地址: 0xc000098040
p 的值: 0xc000098040
p 内存的解值: 10
x 的值: 20
x 的地址: 0xc000098040
p 的值: 0xc000098040
p 内存的解值: 20

📌 核心:
0xc000098040这个内存的地址没有变化

👉 通过指针可以修改原变量


指针 vs 值传递(重点🔥)

值传递

go 复制代码
package main

import "fmt"

func change(x int) {
	fmt.Println("x1 = ", x)
	fmt.Println("&x1 = ", &x)
	x = 100
	fmt.Println("x2 = ", x)
	fmt.Println("&x2 = ", &x)
}
func main() {
	a := 10
	fmt.Println("a1 = ", a)
	fmt.Println("&a1 = ", &a)
	change(a)
	fmt.Println("a2 = ", a)
	fmt.Println("&a2 = ", &a)
}

输出:

bath 复制代码
a1 =  10
&a1 =  0xc000010120
x1 =  10
&x1 =  0xc000010140
x2 =  100
&x2 =  0xc000010140
a2 =  10
&a2 =  0xc000010120

📌 原因:

👉 传的是"副本"


指针传递

go 复制代码
package main

import "fmt"

func change(p *int) {
	fmt.Println("p1 = ", *p)
	fmt.Println("&p1 = ", p)
	*p = 100
	fmt.Println("p2 = ", *p)
	fmt.Println("&p2 = ", p)
}
func main() {
	a := 10
	fmt.Println("a1 = ", a)
	fmt.Println("&a1 = ", &a)
	change(&a)
	fmt.Println("a2 = ", a)
	fmt.Println("&a2 = ", &a)
}

输出:

bath 复制代码
a1 =  10
&a1 =  0xc000010120
p1 =  10
&p1 =  0xc000010120
p2 =  100
&p2 =  0xc000010120
a2 =  100
&a2 =  0xc000010120

📌 原因:

👉 传的是"地址",直接修改原值


指针的零值(非常重要)

go 复制代码
package main

import "fmt"

func main() {
	var p *int
	fmt.Println(p)
}

输出:

bath 复制代码
<nil>

📌 结论:

  • 指针默认值是 nil
  • nil 指针不能解引用

⚠️ 空指针错误

go 复制代码
var p *int
*p = 10 // panic!

👉 报错:

bash 复制代码
panic: runtime error: invalid memory address or nil pointer dereference

翻译:panic:运行时错误:无效内存地址或空指针解引用


正确写法

go 复制代码
package main

import "fmt"

func main() {
	x := 10
	p := &x
	fmt.Println(*p) // 输出 10
	fmt.Println(p) // 输出 0xc000110040
}

或:

go 复制代码
package main

import "fmt"

func main() {
	p := new(int)
	*p = 10
	fmt.Println(*p) // 输出 10
	fmt.Println(p)  // 输出 0xc000010120
}

new vs &(面试常问🔥)

new

go 复制代码
package main

import "fmt"

func main() {
	p := new(int)
	fmt.Println(*p) // 输出 0
	fmt.Println(p)  // 输出 0xc000098040
}

📌 特点:

  • 分配内存
  • 返回指针
  • 默认值为 0

&

go 复制代码
package main

import "fmt"

func main() {
	x := 10
	p := &x
	fmt.Println("*p:", *p) // 输出 10
	fmt.Println("p:", p)   // 输出地址
	fmt.Println("x:", x)   // 输出10
	fmt.Println("&x", &x)  // 输出地址
}

输出:

bath 复制代码
*p: 10
p: 0xc000098040
x: 10
&x 0xc000098040

📌 特点:

  • 获取已有变量地址

对比总结

方式 用途
new 创建变量并返回指针
& 获取已有变量地址

指针与结构体

普通结构体

go 复制代码
package main

import "fmt"

type User struct {
	Name string
}

func main() {
	u := User{Name: "Tom"}
	fmt.Println(u)
	fmt.Println(u.Name)
}

输出:

bath 复制代码
{Tom}
Tom

指针结构体

go 复制代码
package main

import "fmt"

type User struct {
	Name string
}

func main() {
	u := &User{Name: "Tom"}
	fmt.Println("u:", u)
	fmt.Println("*u:", *u)
	fmt.Println("u.Name:", u.Name)
	fmt.Println("(*u).Name:", (*u).Name)
}

输出:

bath 复制代码
u: &{Tom}
*u: {Tom}
u.Name: Tom
(*u).Name: Tom

📌 Go 特性:

👉 自动解引用(语法糖)


修改结构体

go 复制代码
package main

import "fmt"

type User struct {
	Name string
}

func update(u *User) {
	fmt.Println("u.name 3:", u.Name) // Tom
	fmt.Println("&u 3:", &u)
	u.Name = "Jerry"
	fmt.Println("u.name 4:", u.Name) // Jerry
	fmt.Println("&u 4:", &u)
}

func main() {
	u := User{Name: "Tom"}
	fmt.Println("u.name 1:", u.Name) // Tom
	fmt.Println("&u 1:", &u)
	update(&u)
	fmt.Println("&u 2:", &u)
	fmt.Println("u.name 2:", u.Name) // Jerry
}

输出:

bath 复制代码
u.name 1: Tom
&u 1: &{Tom}
u.name 3: Tom
&u 3: 0xc000102000
u.name 4: Jerry
&u 4: 0xc000102000
&u 2: &{Jerry}
u.name 2: Jerry

指针常见坑(必看🔥)

for 循环变量指针坑

go 复制代码
package main

import "fmt"

func main() {
	arr := []int{1, 2, 3}
	var ptrs []*int

	for _, v := range arr {
		ptrs = append(ptrs, &v)
	}

	for _, p := range ptrs {
		fmt.Println(*p)
	}
}

👉 输出:

bash 复制代码
1
2
3
场景 Go 1.21 以前 Go 1.22+
for range 取地址 ❌ 有坑(3 3 3) ✅ 已修复

❗原因

👉 v 是同一个变量复用

✅ 正确写法老版本

go 复制代码
for _, v := range arr {
	tmp := v
	ptrs = append(ptrs, &tmp)
}

什么时候用指针?

推荐使用场景:

✅ 需要修改原数据

go 复制代码
func update(p *int)

✅ 结构体较大(性能优化)

go 复制代码
func handle(u *User)

✅ 避免拷贝(高性能场景)


❌ 不推荐:

  • 小数据类型(int、bool)
  • 不需要修改的场景

总结(面试速记版🔥)

👉 指针核心三点:

  1. & 取地址
  2. * 解引用
  3. 指针可以修改原变量

👉 高频考点:

  • nil 指针会 panic
  • new vs &
  • 值传递 vs 指针传递
  • for range 指针坑
  • 结构体指针自动解引用

结语

Go 的指针相比 C 语言简化了很多:

  • ❌ 没有指针运算
  • ❌ 没有复杂语法
  • ✅ 更安全
  • ✅ 更易用

但它依然是:

👉 写出高性能 Go 程序的关键能力


相关推荐
xyq20242 小时前
jEasyUI 添加工具栏
开发语言
jjjava2.02 小时前
数据库事务:ACID特性与实战应用
java·开发语言·数据库
zzginfo2 小时前
JavaScript 中 Array 、 Set 、 WeakSet 区别
开发语言·javascript·ecmascript
发发就是发2 小时前
顺序锁(Seqlock)与RCU机制:当读写锁遇上性能瓶颈
java·linux·服务器·开发语言·jvm·驱动开发
农村小镇哥2 小时前
PHP递归遍历+MYSQL介绍+MYSQL基本操作
开发语言·mysql·php
llm大模型算法工程师weng2 小时前
Python爬虫实现指南:从入门到实战
开发语言·爬虫·python
lly2024062 小时前
R 绘图 - 函数曲线图
开发语言
菜鸟小九3 小时前
JUC(共享模型之管程、synchronized、wait、park、活跃性、renetrantlock、条件变量)
java·开发语言·juc