新闻中心
Go语言中指针接收器与结构体字段更新的深度解析

本文深入探讨go语言中指针接收器在更新结构体字段时常遇到的问题,特别是当局部指针变量被重新赋值时无法影响原始结构体。通过二叉搜索树的插入操作为例,文章详细解释了指针赋值与指向值修改的区别,并引入了“指针的指针”这一高级概念,展示了如何通过多一层间接引用来正确更新结构体内部的指针字段,从而确保数据结构的持久性修改。
在Go语言中,指针接收器(pointer receiver)是方法定义中常见的模式,它允许方法修改接收器所指向的实际值。然而,对于初学者来说,指针的赋值行为与通过指针修改其指向的值之间的区别常常会造成混淆。本文将通过一个二叉搜索树(BST)的插入操作实例,深入剖析这一常见陷阱,并提供正确的解决方案。
理解Go语言中的指针赋值与值修改
考虑以下二叉搜索树的结构定义:
package main
import "fmt"
// Node 定义二叉树节点
type Node struct {
key int
left, right *Node
}
// NewNode 创建一个新节点
func NewNode(key int) *Node {
return &Node{key, nil, nil}
}
// BST 定义二叉搜索树
type BST struct {
root *Node
}
// NewBinarySearchTree 创建一个空的二叉搜索树
func NewBinarySearchTree() *BST {
return &BST{nil}
}
// Insert 方法:原始的正确插入实现
func (t *BST) Insert(key int) {
if t.root == nil {
t.root = NewNode(key)
return
}
var node = t.root
for {
if key < node.key {
if node.left == nil {
node.left = NewNode(key) // 直接修改了 node.left 指针
return
} else {
node = node.left // 移动到左子节点
}
} else {
if node.right == nil {
node.right = NewNode(key) // 直接修改了 node.right 指针
return
} else {
node = node.right // 移动到右子节点
}
}
}
}
// inorder 中序遍历打印节点
func inorder(node *Node) {
if node == nil {
return
}
inorder(node.left)
fmt.Print(node.key, " ")
inorder(node.right)
}
func main() {
tree := NewBinarySearchTree()
tree.Insert(3)
tree.Insert(1)
tree.Insert(2)
tree.Insert(4)
fmt.Print("原始Insert方法结果: ")
inorder(tree.root) // 预期输出: 1 2 3 4
fmt.Println()
// 尝试使用错误的Insert2方法
tree2 := NewBinarySearchTree()
tree2.Insert2(3) // 期望插入3
fmt.Print("Insert2方法结果: ")
inorder(tree2.root) // 预期输出: 空 (或没有任何输出)
fmt.Println()
// 使用Insert3方法
tree3 := NewBinarySearchTree()
tree3.Insert3(3)
tree3.Insert3(1)
tree3.Insert3(2)
tree3.Insert3(4)
fmt.Print("Insert3方法结果: ")
inorder(tree3.root) // 预期输出: 1 2 3 4
fmt.Println()
}在上述 Insert 方法中,当找到插入位置时,我们直接通过 node.left = NewNode(key) 或 node.right = NewNode(key) 来修改当前节点的左子节点或右子节点指针。这能够正确地更新二叉树结构。
然而,如果尝试对 Insert 方法进行如下“简化”:
// Insert2 方法:错误的插入实现
func (t *BST) Insert2(key int) {
var node *Node
node = t.root // 1. node 变量指向 t.root 所指向的地址
for node != nil {
if key < node.key {
node = node.left // 2. node 变量被重新赋值为 node.left 所指向的地址
} else {
node = node.right // 3. node 变量被重新赋值为 node.right 所指向的地址
}
}
node = NewNode(key) // 4. node 变量被重新赋值为新节点的地址
}使用 Insert2 方法,二叉树将永远不会被更新。原因在于Go语言的赋值行为。当执行 node = t.root 时,node 只是获得了 t.root 所存储的指针值的副本,即它们现在指向同一个内存地址。在循环内部,node = node.left 或 node = node.right 同样只是将 node 这个局部变量重新指向了树中其他节点的子节点。
最关键的一步是 node = NewNode(key)。这一行代码将 node 这个局部变量重新赋值,使其指向了一个全新的 Node 对象。这并不会影响 t.root,也不会影响之前遍历过程中任何节点的 left 或 right 字段,因为 node 只是一个局部变量,它的重新赋值不会回溯到它曾经指向的那些原始指针变量(如 t.root 或 parent.left)。
可以将其想象成:你复制了一份房子的地址给朋友(node = t.root)。朋友去这个地址(遍历),然后决定要买一套新房子,于是他把新房子的地址写在了自己的纸条上(node = NewNode(key))。这并不会改变你纸条上的地址,也不会改变原始房子的地址簿。
易标AI
告别低效手工,迎接AI标书新时代!3分钟智能生成,行业唯一具备查重功能,自动避雷废标项
135
查看详情
解决方案:使用指针的指针 (**Node)
为了解决这个问题,我们需要修改的不是 node 这个局部指针变量本身,而是 t.root、node.left 或 node.right 这些“存储指针的变量”本身。换句话说,我们需要一个指针来指向这些存储指针的变量。这就是“指针的指针”(pointer to pointer)或者说多一层间接引用的概念。
如果我们要修改一个类型为 *Node 的变量(例如 t.root、someNode.left 或 someNode.right)
,那么我们需要一个类型为 **Node 的指针来指向它。
以下是 Insert 方法的正确实现,它利用了指针的指针:
// Insert3 方法:使用指针的指针实现正确插入
func (t *BST) Insert3(key int) {
// node 现在是一个指向 *Node 类型变量的指针,即 **Node
// 初始时,它指向 t.root 的内存地址
node := &t.root
// 循环条件:*node != nil 检查当前 *Node 变量(例如 t.root, parent.left)是否为 nil
for *node != nil {
// key < (*node).key:解引用 node 得到 *Node,然后访问其 key 字段
if key < (*node).key {
// node = &(*node).left:
// 1. 解引用 node 得到当前的 *Node 对象 (例如某个父节点)
// 2. 访问该 *Node 对象的 left 字段 (它是一个 *Node 类型变量)
// 3. 取 left 字段的地址 (&),这个地址是一个 **Node 类型的值
// 4. 将这个 **Node 值赋值给 node 变量,使 node 现在指向 parent.left 的内存地址
node = &(*node).left
} else {
// 同理,使 node 指向 parent.right 的内存地址
node = &(*node).right
}
}
// 循环结束后,node 指向了应该插入新节点的那个 *Node 变量的内存地址
// 例如,如果树为空,node 指向 &t.root
// 如果插入到叶子节点的左侧,node 指向 &parent.left
// *node = NewNode(key):
// 解引用 node,得到它所指向的 *Node 变量(例如 t.root 或 parent.left),
// 然后将新创建的节点赋值给它,从而更新了树的结构。
*node = NewNode(key)
}通过 node := &t.root,我们让 node 变量存储了 t.root 变量的内存地址。因此,node 的类型是 **Node。
在循环中:
- *node != nil:这里 *node 对 node 进行解引用,得到的是 t.root(或其他 *Node 变量)的实际值。我们检查这个实际值是否为 nil。
- if key
- node = &(*node).left:这是核心。(*node) 得到当前节点(例如父节点),.left 访问其左子节点字段(这个字段本身是一个 *Node 类型的变量),然后 & 取这个 left 字段的内存地址。这个地址(类型为 **Node)被赋值回 node。这意味着 node 现在指向了父节点的 left 字段的内存位置。
- 循环结束时,node 变量存储的正是我们想要更新的那个 *Node 变量(可能是 t.root、某个父节点的 left 字段或 right 字段)的内存地址。
- *node = NewNode(key):最后一步,对 node 进行解引用,得到它所指向的 *Node 变量,然后将 NewNode(key) 返回的新节点赋值给它。这样就成功地更新了树的结构。
总结与注意事项
-
指针赋值与值修改的区别:
- ptr = anotherPtr:这只是将 anotherPtr 的值(一个内存地址)复制给 ptr。ptr 现在指向与 anotherPtr 相同的位置,但它们仍然是两个独立的指针变量。对 ptr 后续的重新赋值(如 ptr = NewValue())不会影响 anotherPtr 或 ptr 曾经指向的原始变量。
- *ptr = newValue:这才是通过 ptr 修改它所指向的内存位置上的值。newValue 将被写入 ptr 所指向的地址。
-
何时需要指针的指针:
- 当你需要修改一个已经存在的指针变量本身(而不是它所指向的值)时,例如将其从 nil 变为指向某个对象,或者将其从指向对象A变为指向对象B,并且这个修改需要对调用者可见(即影响原始变量),你就需要一个指向该指针变量的指针。
- 在数据结构(如链表、树)中,当需要更新 root、next、left、right 等指针字段以连接新节点或移除旧节点时,这种模式非常有用。
- *清晰使用 `和&`**:
- & (Address-of Operator):获取一个变量的内存地址。
- * (Dereference Operator):访问指针所指向的内存地址上的值。
理解Go语言中指针的赋值语义和多级指针的使用是编写高效、正确数据结构操作的关键。通过上述示例,希望能帮助读者更深入地掌握Go语言中指针接收器的工作原理及其在复杂数据结构操作中的应用。
以上就是Go语言中指针接收器与结构体字段更新的深度解析的详细内容,更多请关注其它相关文章!
# 到它
# 脸书有没有网站推广
# 晋州百度网站推广价钱
# 阜阳产品seo推广
# 吉安网站建设网站制作
# 青岛抖音营销推广代理商
# 商务网站建设推广服务
# 绵阳优化团队资讯网站
# 数字通信关键词排名软件
# 外贸企业seo优化
# 会计学习网站建设
# 如何使用
# 二叉树
# node
# 值为
# 这一
# 将其
# 遍历
# 链表
# 是一个
# 数据结构
# 区别
# ai
# go语言
# go
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
漫蛙漫画网页端入口 漫蛙2官方正版漫画站点
在J*a中如何捕获IndexOutOfBoundsException_索引越界异常防护方法说明
J*aScript中localStorage数据的获取、清洗与格式化教程
淘宝网网页版登录入口 淘宝官方网页版快捷登录
Bilibili动漫最新防封地址发布-Bilibili动漫2025年最稳正版入口推荐
Golang如何优化CPU绑定任务分配策略_Golang CPU任务分配优化实践
搜狗浏览器如何使用密码生成器创建强密码 搜狗浏览器内置密码安全工具
J*aScript设计模式实践_j*ascript代码优化
抖音从哪里进入网页版_抖音官方入口链接
c++ 获取系统当前时间 c++时间戳获取方法
Yandex官网搜索引擎免登录_俄罗斯Yandex一键直达入口
微博网页版首页入口 微博电脑端官网登录链接
Basecamp怎样用留言钉固定重点_Basecamp用留言钉固定重点【重点标记】
uc浏览器网页版入口 uc浏览器网页版最新网址
QQ网页版官方账号入口 QQ网页版网页版登录指南
sublime怎么预览Markdown渲染效果_Markdown Preview插件 for sublime教程
抖音网页版企业服务中心登录入口_抖音网页版企业登录平台
qq游戏跨平台入口_qq游戏多设备同步登录
漫蛙漫画官方首页 漫蛙2漫画在线阅读入口
Yandex免登录官网入口_俄罗斯Yandex搜索引擎直达链接
解决macOS上安装pyhdf时‘hdf.h’文件缺失的编译错误
在WordPress中通过REST API获取BasicAuth保护的远程文章
如何使用CaptainHook和Composer管理Git钩子_在提交前自动运行代码检查的Composer配置
PyTorch模型训练效果不佳?深入剖析常见错误与调试技巧
mcjs网页版流畅运行 mcjs低配电脑畅玩入口
在J*a中如何开发简易博客标签推荐系统_博客标签推荐项目实战解析
百度网盘网页版入口 百度网盘网页版官方登录网址
J*aScript类型检查_j*ascript代码规范
新三国志曹操传110级星符试炼夏侯渊极难攻略
Lar*el Excel导入时生成自定义递增ID的策略与实践
Excel函数批量查找替换超快方法_Excel用REPLACE和FIND函数秒级替换
Mac怎么锁定备忘录_Mac备忘录加密设置教程
Sublime Text怎么显示空格和制表符_Sublime显示不可见字符设置
sublime如何优雅地处理行尾空格_sublime自动清理多余空白字符配置
J*aScript中针对特定容器内图片动画的实现教程
怎么在html里运行vbs脚本_html中运行vbs脚本方法【教程】
Android Studio计算器C键功能异常排查与修复教程
拼多多购物车商品数量无法修改如何处理 拼多多购物车操作优化方法
抖音网页版平台入口 抖音网页版官网在线访问教程
J*a递归快速排序中静态变量导致数据累积的陷阱与解决方案
sublime如何处理大型CSV文件的列对齐_sublime高级表格编辑插件指南
最新韩小圈网页版登录入口_官网在线观看官方链接
J*aScript动态修改指定div内所有a标签样式指南
J*aScript中正确使用querySelectorAll与复杂CSS选择器
铁路12306改签能改到更早的车次吗_铁路12306改签提前车次规则
如何使 Jest 模拟函数默认抛出错误以提高测试效率
uc手机浏览器网页版入口 uc浏览器手机版便捷登录首页
Win11文件资源管理器卡顿怎么修 Win11重置资源管理器进程优化响应速度【修复方法】
漫蛙官网正版漫画入口 漫蛙2官方网页登录地址
AO3同人作品网入口 AO3搜索引擎官网永久地址


2025-11-08
浏览次数:次
返回列表