指针
指针是Go中理解内存和性能的关键。别怕,Go的指针比C简单多了!我当年学C语言的时候,指针这一章学得特别痛苦。但Go的指针设计得很优雅,没有指针运算,也不能随意访问内存,安全多了!
什么是指针?
指针是存储另一个变量内存地址的变量。
// 变量直接存储值
var a int = 42
// 指针存储变量的地址
var p *int = &a // p是指向int的指针,存储a的地址
图示:
变量a: 值: 42 地址: 0xc0000140a0
指针p: 值: 0xc0000140a0 (存的是a的地址)
基本语法
var x int = 10
// & 取地址
p := &x // p是*int类型,存储x的地址
// * 解引用(获取指针指向的值)
fmt.Println(*p) // 10(获取p指向的值)
// 通过指针修改值
*p = 20
fmt.Println(x) // 20(x也变了!)
类型说明
var x int = 10
var p *int = &x // p的类型是 *int(指向int的指针)
fmt.Printf("x的值: %d\n", x) // 10
fmt.Printf("x的地址: %p\n", &x) // 0xc0000140a0
fmt.Printf("p的值: %p\n", p) // 0xc0000140a0(和x的地址一样)
fmt.Printf("p指向的值: %d\n", *p) // 10
为什么需要指针?
1. 修改函数参数
Go是值传递,函数参数是副本。想修改原变量,必须用指针!
// ❌ 不能修改原变量
func double(n int) {
n = n * 2 // 只是修改副本
}
// ✅ 使用指针修改原变量
func doublePtr(n *int) {
*n = *n * 2 // 修改指针指向的值
}
func main() {
x := 10
double(x)
fmt.Println(x) // 10(没变)
doublePtr(&x)
fmt.Println(x) // 20(变了!)
}
2. 避免大数据复制
type BigData struct {
Data [1000000]int // 很大的数组
}
// ❌ 值传递会复制整个结构体(约4MB)
func processBad(d BigData) {
// ...
}
// ✅ 传指针只复制8字节(地址大小)
func processGood(d *BigData) {
// ...
}
3. 表示"无值"
func find(id int) *User {
// 找到返回指针
if found {
return &user
}
// 没找到返回nil
return nil
}
// 使用
user := find(123)
if user == nil {
fmt.Println("用户不存在")
} else {
fmt.Println(user.Name)
}
new 和 make
new
new(T) 分配内存,返回 *T 类型指针,内存初始化为零值。
// new返回指针
p := new(int) // *int类型,值为0
fmt.Println(*p) // 0
q := new(Person) // *Person类型,所有字段为零值
make
make 只用于切片、map、channel,返回初始化好的值(不是指针)。
s := make([]int, 5) // []int类型,长度5
m := make(map[string]int) // map[string]int类型
c := make(chan int) // chan int类型
new vs make 对比
| 特性 | new | make |
|---|---|---|
| 返回类型 | 指针 *T | 值 T |
| 适用类型 | 任何类型 | slice、map、chan |
| 初始化 | 零值 | 完全初始化 |
// new 和字面量创建指针等价
p1 := new(Person)
p2 := &Person{} // 更常用
指针的零值
指针的零值是 nil。使用nil指针会panic!
var p *int
fmt.Println(p) // <nil>
// *p = 10 // ❌ panic: runtime error: invalid memory address
// 使用前要检查
if p != nil {
fmt.Println(*p)
}
函数中的指针
传值 vs 传指针
// 传值:创建副本
func modifyValue(s Student) {
s.Age = 100 // 只修改副本
}
// 传指针:操作原数据
func modifyPointer(s *Student) {
s.Age = 100 // 修改原数据
}
func main() {
s := Student{Name: "张三", Age: 20}
modifyValue(s)
fmt.Println(s.Age) // 20(没变)
modifyPointer(&s)
fmt.Println(s.Age) // 100(变了)
}
返回局部变量的指针
在Go中完全安全!Go会自动将变量分配到堆上(逃逸分析)。
// ✅ 完全安全
func newPerson(name string) *Person {
p := Person{Name: name}
return &p // 返回局部变量的指针
}
// Go编译器会自动把p分配到堆上,不用担心
和C的区别
在C语言中,返回局部变量的指针是危险的(野指针)。 Go有垃圾回收,自动处理内存,你可以放心使用!
指针和结构体
type Person struct {
Name string
Age int
}
func main() {
// 创建结构体
p := Person{Name: "张三", Age: 25}
// 获取指针
ptr := &p
// 通过指针访问字段(Go自动解引用)
fmt.Println(ptr.Name) // 张三
// 等价于
fmt.Println((*ptr).Name) // 张三
// 修改
ptr.Age = 26
fmt.Println(p.Age) // 26
}
指针数组 vs 数组指针
// 指针数组:存储指针的数组
var ptrArr [3]*int
a, b, c := 1, 2, 3
ptrArr = [3]*int{&a, &b, &c}
// 数组指针:指向数组的指针
arr := [3]int{1, 2, 3}
var arrPtr *[3]int = &arr
二级指针
指向指针的指针(用得不多):
a := 10
p := &a // 一级指针
pp := &p // 二级指针
fmt.Println(**pp) // 10
// 修改
**pp = 20
fmt.Println(a) // 20
常见使用场景
1. 方法接收者
type Counter struct {
count int
}
// 值接收者:不能修改
func (c Counter) GetCount() int {
return c.count
}
// 指针接收者:可以修改
func (c *Counter) Increment() {
c.count++
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println(c.GetCount()) // 2
}
2. 链表结构
type Node struct {
Value int
Next *Node // 指向下一个节点
}
func main() {
// 创建链表: 1 -> 2 -> 3
n1 := &Node{Value: 1}
n2 := &Node{Value: 2}
n3 := &Node{Value: 3}
n1.Next = n2
n2.Next = n3
// 遍历链表
for n := n1; n != nil; n = n.Next {
fmt.Println(n.Value)
}
}
3. 树结构
type TreeNode struct {
Value int
Left *TreeNode
Right *TreeNode
}
// 创建二叉树
// 1
// / \
// 2 3
root := &TreeNode{
Value: 1,
Left: &TreeNode{Value: 2},
Right: &TreeNode{Value: 3},
}
实战案例:实现链表
package main
import "fmt"
// 链表节点
type ListNode struct {
Value int
Next *ListNode
}
// 链表
type LinkedList struct {
Head *ListNode
size int
}
// 创建新链表
func NewLinkedList() *LinkedList {
return &LinkedList{}
}
// 添加到末尾
func (l *LinkedList) Append(value int) {
newNode := &ListNode{Value: value}
if l.Head == nil {
l.Head = newNode
} else {
current := l.Head
for current.Next != nil {
current = current.Next
}
current.Next = newNode
}
l.size++
}
// 在头部添加
func (l *LinkedList) Prepend(value int) {
newNode := &ListNode{Value: value, Next: l.Head}
l.Head = newNode
l.size++
}
// 删除节点
func (l *LinkedList) Delete(value int) bool {
if l.Head == nil {
return false
}
// 删除头节点
if l.Head.Value == value {
l.Head = l.Head.Next
l.size--
return true
}
// 删除其他节点
current := l.Head
for current.Next != nil {
if current.Next.Value == value {
current.Next = current.Next.Next
l.size--
return true
}
current = current.Next
}
return false
}
// 查找节点
func (l *LinkedList) Find(value int) *ListNode {
current := l.Head
for current != nil {
if current.Value == value {
return current
}
current = current.Next
}
return nil
}
// 获取长度
func (l *LinkedList) Size() int {
return l.size
}
// 转换为切片
func (l *LinkedList) ToSlice() []int {
result := []int{}
current := l.Head
for current != nil {
result = append(result, current.Value)
current = current.Next
}
return result
}
// 打印链表
func (l *LinkedList) Print() {
current := l.Head
for current != nil {
fmt.Print(current.Value)
if current.Next != nil {
fmt.Print(" -> ")
}
current = current.Next
}
fmt.Println()
}
// 反转链表
func (l *LinkedList) Reverse() {
var prev *ListNode
current := l.Head
for current != nil {
next := current.Next
current.Next = prev
prev = current
current = next
}
l.Head = prev
}
func main() {
list := NewLinkedList()
// 添加元素
list.Append(1)
list.Append(2)
list.Append(3)
list.Prepend(0)
fmt.Print("链表: ")
list.Print() // 0 -> 1 -> 2 -> 3
fmt.Printf("长度: %d\n", list.Size()) // 4
// 查找
if node := list.Find(2); node != nil {
fmt.Printf("找到节点: %d\n", node.Value)
}
// 删除
list.Delete(2)
fmt.Print("删除2后: ")
list.Print() // 0 -> 1 -> 3
// 反转
list.Reverse()
fmt.Print("反转后: ")
list.Print() // 3 -> 1 -> 0
fmt.Println("转切片:", list.ToSlice()) // [3 1 0]
}
输出:
链表: 0 -> 1 -> 2 -> 3
长度: 4
找到节点: 2
删除2后: 0 -> 1 -> 3
反转后: 3 -> 1 -> 0
转切片: [3 1 0]
实战案例:实现二叉搜索树
package main
import "fmt"
// 二叉搜索树节点
type BSTreeNode struct {
Value int
Left *BSTreeNode
Right *BSTreeNode
}
// 二叉搜索树
type BSTree struct {
Root *BSTreeNode
}
// 创建新树
func NewBSTree() *BSTree {
return &BSTree{}
}
// 插入节点
func (t *BSTree) Insert(value int) {
t.Root = insertNode(t.Root, value)
}
func insertNode(node *BSTreeNode, value int) *BSTreeNode {
if node == nil {
return &BSTreeNode{Value: value}
}
if value < node.Value {
node.Left = insertNode(node.Left, value)
} else if value > node.Value {
node.Right = insertNode(node.Right, value)
}
return node
}
// 查找节点
func (t *BSTree) Search(value int) bool {
return searchNode(t.Root, value)
}
func searchNode(node *BSTreeNode, value int) bool {
if node == nil {
return false
}
if value == node.Value {
return true
}
if value < node.Value {
return searchNode(node.Left, value)
}
return searchNode(node.Right, value)
}
// 中序遍历(有序输出)
func (t *BSTree) InOrder() []int {
result := []int{}
inOrderTraverse(t.Root, &result)
return result
}
func inOrderTraverse(node *BSTreeNode, result *[]int) {
if node == nil {
return
}
inOrderTraverse(node.Left, result)
*result = append(*result, node.Value)
inOrderTraverse(node.Right, result)
}
// 获取最小值
func (t *BSTree) Min() (int, bool) {
if t.Root == nil {
return 0, false
}
node := t.Root
for node.Left != nil {
node = node.Left
}
return node.Value, true
}
// 获取最大值
func (t *BSTree) Max() (int, bool) {
if t.Root == nil {
return 0, false
}
node := t.Root
for node.Right != nil {
node = node.Right
}
return node.Value, true
}
func main() {
tree := NewBSTree()
// 插入节点
values := []int{50, 30, 70, 20, 40, 60, 80}
for _, v := range values {
tree.Insert(v)
}
// 50
// / \
// 30 70
// / \ / \
// 20 40 60 80
fmt.Println("中序遍历(有序):", tree.InOrder()) // [20 30 40 50 60 70 80]
fmt.Println("查找40:", tree.Search(40)) // true
fmt.Println("查找45:", tree.Search(45)) // false
if min, ok := tree.Min(); ok {
fmt.Println("最小值:", min) // 20
}
if max, ok := tree.Max(); ok {
fmt.Println("最大值:", max) // 80
}
}
指针使用建议
什么时候用指针?
| 场景 | 是否用指针 | 原因 |
|---|---|---|
| 需要修改参数 | ✅ 用指针 | 值传递无法修改原变量 |
| 大结构体 | ✅ 用指针 | 避免大量复制 |
| 小结构体(几个字段) | ❓ 视情况 | 值传递也可以 |
| 基本类型(int、string) | ❌ 不用 | 复制开销小 |
| 切片、map、channel | ❌ 不用 | 本身就是引用类型 |
常见错误
// ❌ 错误:使用nil指针
var p *int
*p = 10 // panic!
// ✅ 正确:先初始化
p := new(int)
*p = 10
// ❌ 错误:循环中取地址
var ptrs []*int
for i := 0; i < 3; i++ {
ptrs = append(ptrs, &i) // 都指向同一个i!
}
// ✅ 正确:创建新变量
for i := 0; i < 3; i++ {
v := i
ptrs = append(ptrs, &v)
}
练习
- 写一个函数,交换两个整数的值(使用指针)
- 实现一个函数,找出切片中的最大值,返回其指针(找不到返回nil)
- 实现双向链表的基本操作
参考答案
package main
import "fmt"
// 1. 交换两个整数
func swap(a, b *int) {
*a, *b = *b, *a
}
// 2. 找最大值返回指针
func findMax(nums []int) *int {
if len(nums) == 0 {
return nil
}
maxIdx := 0
for i, n := range nums {
if n > nums[maxIdx] {
maxIdx = i
}
}
return &nums[maxIdx]
}
// 3. 双向链表
type DNode struct {
Value int
Prev *DNode
Next *DNode
}
type DoublyLinkedList struct {
Head *DNode
Tail *DNode
size int
}
func NewDoublyLinkedList() *DoublyLinkedList {
return &DoublyLinkedList{}
}
func (l *DoublyLinkedList) Append(value int) {
node := &DNode{Value: value}
if l.Tail == nil {
l.Head = node
l.Tail = node
} else {
node.Prev = l.Tail
l.Tail.Next = node
l.Tail = node
}
l.size++
}
func (l *DoublyLinkedList) Prepend(value int) {
node := &DNode{Value: value}
if l.Head == nil {
l.Head = node
l.Tail = node
} else {
node.Next = l.Head
l.Head.Prev = node
l.Head = node
}
l.size++
}
func (l *DoublyLinkedList) PrintForward() {
current := l.Head
for current != nil {
fmt.Print(current.Value)
if current.Next != nil {
fmt.Print(" <-> ")
}
current = current.Next
}
fmt.Println()
}
func (l *DoublyLinkedList) PrintBackward() {
current := l.Tail
for current != nil {
fmt.Print(current.Value)
if current.Prev != nil {
fmt.Print(" <-> ")
}
current = current.Prev
}
fmt.Println()
}
func main() {
// 1. 测试swap
a, b := 10, 20
swap(&a, &b)
fmt.Printf("交换后: a=%d, b=%d\n", a, b) // a=20, b=10
// 2. 测试findMax
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
if max := findMax(nums); max != nil {
fmt.Printf("最大值: %d\n", *max) // 9
}
// 3. 测试双向链表
dll := NewDoublyLinkedList()
dll.Append(1)
dll.Append(2)
dll.Append(3)
dll.Prepend(0)
fmt.Print("正向: ")
dll.PrintForward() // 0 <-> 1 <-> 2 <-> 3
fmt.Print("反向: ")
dll.PrintBackward() // 3 <-> 2 <-> 1 <-> 0
}
🎉 恭喜你完成了基础篇!你已经掌握了Go的核心语法。
接下来进入 进阶篇,学习更强大的特性:方法、接口、并发编程!
