Golang的指针机制为链式数据结构的实现提供了基础支持,链表作为典型的链式结构,其节点之间的关联完全依赖指针来完成,合理的指针设计能让链式结构的操作更高效、逻辑更清晰。

Golang指针基础回顾
Golang的指针用于存储变量的内存地址,通过*操作符可以声明指针类型,通过&操作符可以获取变量的地址。和C语言不同,Golang的指针不支持指针运算,也没有野指针问题,由垃圾回收机制自动管理指针指向的内存。
比如声明一个整型指针的代码如下:
package main
import "fmt"
func main() {
var num int = 10
var p *int = &num // p是指向num的指针
fmt.Println(*p) // 解引用获取num的值,输出10
}
链表节点的指针设计
链表的核心是节点,每个节点需要存储自身的数据,同时存储指向下一个节点的指针,在Golang中可以通过结构体来定义链表节点,指针字段的类型是结构体自身的指针。
单链表节点定义
单链表每个节点只有一个指向下一节点的指针,定义方式如下:
// 定义单链表节点结构体
type ListNode struct {
Val int // 节点存储的数据
Next *ListNode // 指向下一个节点的指针,类型为*ListNode
}
双向链表节点定义
双向链表每个节点有两个指针,分别指向前一个和后一个节点,定义方式如下:
// 定义双向链表节点结构体
type DoublyListNode struct {
Val int // 节点存储的数据
Prev *DoublyListNode // 指向前一个节点的指针
Next *DoublyListNode // 指向后一个节点的指针
}
链表操作的指针设计
链表初始化
初始化链表时,头节点可以是一个空指针,也可以是一个带有哨兵值的节点,两种方式都依赖指针的零值特性。
// 初始化一个空单链表,头节点为nil
var head *ListNode
// 初始化带有哨兵节点的单链表
sentinel := &ListNode{Val: -1} // 哨兵节点不存储有效数据
head = sentinel
节点插入操作
插入节点时需要调整指针的指向,以在单链表头部插入节点为例:
// 在单链表头部插入新节点
func insertAtHead(head *ListNode, val int) *ListNode {
newNode := &ListNode{Val: val} // 创建新节点
newNode.Next = head // 新节点的Next指向原来的头节点
return newNode // 返回新的头节点
}
如果是在链表中间插入节点,需要先找到目标位置的前一个节点,再调整指针:
// 在目标节点后插入新节点,prev是目标位置的前一个节点
func insertAfter(prev *ListNode, val int) {
if prev == nil {
return
}
newNode := &ListNode{Val: val}
newNode.Next = prev.Next // 新节点指向prev原来的下一个节点
prev.Next = newNode // prev指向新节点
}
节点删除操作
删除节点时同样需要调整指针的指向,以删除单链表中某个值为val的节点为例:
// 删除单链表中第一个值为val的节点
func deleteNode(head *ListNode, val int) *ListNode {
if head == nil {
return nil
}
// 如果头节点就是要删除的节点
if head.Val == val {
return head.Next
}
cur := head
// 遍历找到要删除节点的前一个节点
for cur.Next != nil && cur.Next.Val != val {
cur = cur.Next
}
// 如果找到了目标节点
if cur.Next != nil {
cur.Next = cur.Next.Next // 跳过要删除的节点
}
return head
}
链表遍历操作
遍历链表时通过指针依次移动当前节点即可,直到指针为nil:
// 遍历单链表并打印所有节点的值
func traverseList(head *ListNode) {
cur := head
for cur != nil {
fmt.Print(cur.Val, " ")
cur = cur.Next // 指针移动到下一个节点
}
fmt.Println()
}
指针设计注意事项
- 操作指针前一定要判断指针是否为nil,避免空指针解引用导致程序崩溃,比如遍历链表时先判断
cur != nil再访问cur.Val。 - 修改链表结构时,注意指针的赋值顺序,比如插入节点时先让新节点指向后续节点,再让前序节点指向新节点,避免链表断裂。
- 不要返回局部变量的指针,Golang虽然会逃逸分析把局部变量分配到堆上,但尽量保持指针指向的是明确生命周期的变量,减少不必要的理解成本。
- 双向链表操作时要同时维护Prev和Next两个指针,避免出现指针指向错误导致链表逻辑混乱。
常见误区说明
误区:认为Golang的指针和C语言指针用法完全一致,随意进行指针运算。实际上Golang不支持指针运算,对指针的操作仅限于取地址、解引用和传递,不要尝试对指针做加减操作。
误区:链表操作中忘记更新头节点指针。比如在头部插入节点后,如果没有把新的头节点赋值给原来的头指针变量,会导致链表结构没有正确更新。
| 操作场景 | 指针设计要点 |
|---|---|
| 节点定义 | 指针字段类型为该结构体自身的指针 |
| 头部插入 | 新节点Next指向原头节点,返回新节点作为头节点 |
| 中间插入 | 先让新节点指向后续节点,再让前序节点指向新节点 |
| 节点删除 | 找到前序节点,修改其Next指针跳过目标节点 |
| 遍历操作 | 通过指针依次移动,每次移动前判断当前指针是否为nil |