新闻详情

新闻详情

首页 / 资讯中心 / 详情

GO [ 指针 ]

发布时间:2026/9/25 19:51:29来源:尧图网络
GO [ 指针 ]
前面我们已经学习了 Go 的变量、常量、数据类型、输入输出、条件控制、切片、字符串和映射表。接下来开始学习 Go 语言中连接变量和存储位置的重要概念指针。按照 Go 官方语言规范 的定义指针类型表示指向某种基础类型变量的所有指针。未初始化指针的零值是nil。简单理解普通变量保存值指针变量保存另一个变量的地址通过指针可以间接读取或修改那个变量。Go 的指针和 C 语言有明显区别普通指针带有明确的基础类型不能进行普通指针加减不需要手动free由垃圾回收器管理对象生命周期可以安全返回局部变量的地址unsafe.Pointer只适合严格受控的底层代码。本章按照“官方定义 → 实际代码 → 运行结果 → 数据结构实现 → 避坑总结”的顺序展开。取地址和解引用使用获取变量地址使用*访问指针指向的值。package main import fmt func main() { value : 10 pointer : value fmt.Println(value:, value) fmt.Println(pointer:, pointer) fmt.Println(*pointer:, *pointer) *pointer 20 fmt.Println(修改后:, value) }运行结果value: 10 pointer: 0x14000122018 *pointer: 10 修改后: 20地址部分会因运行环境不同而变化。这里的重点是value类型是intvalue类型是*intpointer保存value的地址*pointer表示访问地址对应的变量。通过*pointer 20修改的是原变量而不是创建一个新的整数副本。指针类型指针类型使用*T表示var intPointer *int var stringPointer *string type User struct { Name string } var userPointer *User不同基础类型的指针不能直接混用number : 10 pointer : number // var p *string pointer // 编译错误Go 要求普通指针的基础类型明确这可以让编译器阻止很多错误的内存访问。nil 指针指针的零值是nilvar pointer *int fmt.Println(pointer nil)运行结果true解引用前应该判断 nilvar pointer *int if pointer ! nil { fmt.Println(*pointer) } else { fmt.Println(指针为空) }直接解引用 nil 指针会产生运行时 panicvar pointer *int // fmt.Println(*pointer) // panicnil 指针不是“指向 0 的指针”而是没有指向有效变量。指针作为函数参数Go 的普通参数是值传递func change(value int) { value 100 } number : 10 change(number) fmt.Println(number) // 10如果想让函数修改调用者的变量可以传入指针package main import fmt func change(value *int) { *value 100 } func main() { number : 10 change(number) fmt.Println(number) }运行结果100函数接收的是地址解引用后修改的就是调用者原变量。指针参数的 nil 检查func reset(value *int) { if value nil { return } *value 0 }如果 API 允许 nil应当明确处理如果不允许应在文档和调用约定中写清楚。返回指针Go 允许返回局部变量地址C 这么写返回局部变量的栈地址是 UB未定义行为但 Go 这段代码是完全合法、安全的。func createValue() *int { value : 42 return value } func main() { pointer : createValue() fmt.Println(*pointer) }运行结果42编译器会根据逃逸分析决定变量存放位置保证返回后的指针仍然有效。开发者不需要手动释放这块内存。指针和结构体指针经常用于修改结构体package main import fmt type User struct { Name string Score int } func updateScore(user *User, score int) { user.Score score } func main() { user : User{Name: rose} updateScore(user, 95) fmt.Printf(%v\n, user) }运行结果{Name:rose Score:95}访问指针指向的结构体字段时Go 允许省略显式解引用userPointer : User{Name: rose} userPointer.Score 95它等价于(*userPointer).Score 95指针和数组数组是值类型传参会复制整个数组func changeArray(array [3]int) { array[0] 100 } numbers : [3]int{1, 2, 3} changeArray(numbers) fmt.Println(numbers) // [1 2 3]传入数组指针则可以修改原数组func changeArray(array *[3]int) { array[0] 100 } numbers : [3]int{1, 2, 3} changeArray(numbers) fmt.Println(numbers) // [100 2 3]如果数据需要动态长度普通业务代码通常优先使用切片数组指针更适合固定大小且需要避免复制的场景。指针和切片切片本身已经包含底层数组引用修改切片元素通常不需要传递*[]Tfunc changeFirst(numbers []int) { numbers[0] 100 } numbers : []int{1, 2, 3} changeFirst(numbers) fmt.Println(numbers) // [100 2 3]但函数如果需要改变切片长度应返回新的切片func addValue(numbers []int, value int) []int { return append(numbers, value) } numbers addValue(numbers, 4)也可以使用切片指针func addValue(numbers *[]int, value int) { *numbers append(*numbers, value) }在普通 Go 代码中返回新切片通常更清晰。指针不是越多越好切片和 map 已经具有引用语义。new和make内置函数new(T)会创建一个T类型的零值变量并返回*Tpackage main import fmt func main() { pointer : new(int) fmt.Println(*pointer) *pointer 100 fmt.Println(*pointer) }运行结果0 100new(int)的类型是*int不是int。函数适用类型返回值new(T)任意类型指向零值的*Tmake(T)slice、map、channel已初始化的Tpointer : new([]int) // *[]int指向 nil 切片 numbers : make([]int, 0) // []int可直接 append scores : make(map[string]int) messages : make(chan string)不要为了“申请内存”而机械使用new。很多场景直接使用结构体零值、复合字面量或make更符合 Go 风格。指针接收者方法可以使用值接收者或指针接收者。type Counter struct { value int } func (c Counter) Value() int { return c.value } func (c *Counter) Increment() { c.value }完整测试package main import fmt type Counter struct { value int } func (c Counter) Value() int { return c.value } func (c *Counter) Increment() { c.value } func main() { counter : Counter{} counter.Increment() counter.Increment() fmt.Println(counter.Value()) }运行结果2当变量可寻址时counter.Increment()会被编译器自动视为(counter).Increment()。但临时值通常不能自动取地址// Counter{}.Increment() // 编译错误什么时候使用指针接收者通常有以下情况方法需要修改接收者结构体较大需要避免复制类型的方法集合需要统一接收者包含不应该复制的状态。如果方法不修改对象、类型很小且值语义更清晰可以使用值接收者。指针和接口如果方法使用指针接收者通常只有*T实现接口T不一定实现type Runner interface { Run() } type Task struct{} func (t *Task) Run() {} var task Task var runner Runner task // var runner Runner task // 编译错误可以使用编译期断言var _ Runner (*Task)(nil)这不会创建实际业务对象只是让编译器检查接口实现。接口实现规则类型 X 实现接口当且仅当 X 的方法集合包含接口里所有方法。type Runner interface { Run() } type Task struct{} // 指针接收者 func (t *Task) Run() {}*Task的方法集合Run()✅ →*Task实现 RunnerTask值类型的方法集合空❌ →Task没有实现 Runner注意之前那个counter.Increment()的语法糖只在调用方法时生效接口赋值不适用 重点区分两个场景很多人在这里踩坑方法调用counter.Increment()counter 是可寻址变量编译器自动转(counter).Increment()语法糖接口变量赋值var r Runner counter语法糖不会触发这里看的是类型本身有没有实现接口。所以var task Task task.Run() // ✅ 可以调用方法调用语法糖自动转 (task).Run() var r Runner task // ❌ 编译报错接口赋值不做这个自动取地址这是最容易混淆的点task.Run()能跑只是方法调用的语法糖不代表Task类型实现了 Runner只是临时帮你取地址调用方法而已。那这句断言代码是什么var _ Runner (*Task)(nil)拆解_空标识符这个变量不会被使用不会分配内存(*Task)(nil)nil 的 *Task 指针没有创建 Task 结构体对象nil 指针而已整行含义尝试把*Task类型的值赋值给Runner接口变量运行时完全没有开销这行代码编译完成后直接被丢弃不会执行。它唯一作用编译阶段强制编译器检查*Task是否实现 Runner 接口。如果后续改代码不小心把Run()删掉或者改了签名// 删掉 Run() type Task struct{} // func (t *Task) Run() {}那var _ Runner (*Task)(nil)这一行编译直接报错提前发现接口没实现而不是等到业务代码使用的时候才报错。对比如果不写这行断言编译器不会主动检查接口实现。只有当我们真正写var r Runner task的时候才会报错。 很多项目写这个断言放在包里面用来显式声明我们打算让 *Task 实现 Runner强制编译期校验。指针链指针可以指向另一个指针value : 10 first : value second : first fmt.Println(**second)运行结果10second是**int解引用两次才能得到最终的 int。多级指针适合底层场景业务代码中应尽量保持层级简单。垃圾回收和逃逸分析Go 使用垃圾回收器。只要对象仍然可以从活动引用链访问就不会被回收当对象不再可达运行时会在合适时间回收。因此不需要手动free返回局部变量地址是安全的指针只是生命周期关系的一部分应避免无意义地长期保存指针。可以使用编译器参数查看逃逸分析go build -gcflags-m main.go例如func create() *int { value : 10 return value }编译器可能报告value escapes to heap。这不是错误而是为了保证返回后的指针仍然有效。不要为了避免“堆分配”而拒绝使用指针。应该先写清晰正确的代码再通过基准测试和 profile 判断是否真的存在性能问题。unsafe.Pointer的边界unsafe.Pointer可以在不同指针类型之间转换绕过部分类型检查package main import ( fmt unsafe ) func main() { value : int64(10) pointer : (*uint64)(unsafe.Pointer(value)) fmt.Println(*pointer) }这类代码非常底层可能影响类型安全和垃圾回收。官方文档明确要求unsafe.Pointer只能使用规定的安全转换模式并建议运行go vet。普通业务代码不应该使用unsafe.Pointer替代正常类型转换也不应该把指针随意转换成uintptr后长期保存。使用指针实现单链表指针最经典的数据结构应用之一是链表head │ ▼ [value: 10] - [value: 20] - [value: 30] - nil定义节点和链表type node[T any] struct { value T next *node[T] } type LinkedList[T any] struct { head *node[T] size int }head指向第一个节点每个节点的next指向下一个节点最后一个节点的next是 nil。头部插入func (list *LinkedList[T]) PushFront(value T) { list.head node[T]{ value: value, next: list.head, } list.size }新节点先指向旧 head再把 head 更新为新节点。尾部追加func (list *LinkedList[T]) PushBack(value T) { newNode : node[T]{value: value} if list.head nil { list.head newNode list.size return } current : list.head for current.next ! nil { current current.next } current.next newNode list.size }这种实现每次追加都要从头走到尾时间复杂度是 O(n)。如果额外保存tail指针可以把尾部追加优化到 O(1)。删除第一个匹配值泛型T不一定支持比较因此让调用者传入比较函数func (list *LinkedList[T]) RemoveFirst(equal func(T) bool) bool { var previous *node[T] current : list.head for current ! nil { if equal(current.value) { if previous nil { list.head current.next } else { previous.next current.next } var zero T current.value zero current.next nil list.size-- return true } previous current current current.next } return false }删除头节点时修改list.head删除中间节点时让前一个节点跳过当前节点直接连接下一个节点。完整实现linked_list.gopackage main import fmt type node[T any] struct { value T next *node[T] } type LinkedList[T any] struct { head *node[T] size int } func (list *LinkedList[T]) PushFront(value T) { list.head node[T]{value: value, next: list.head} list.size } func (list *LinkedList[T]) PushBack(value T) { newNode : node[T]{value: value} if list.head nil { list.head newNode list.size return } current : list.head for current.next ! nil { current current.next } current.next newNode list.size } func (list *LinkedList[T]) RemoveFirst(equal func(T) bool) bool { var previous *node[T] current : list.head for current ! nil { if equal(current.value) { if previous nil { list.head current.next } else { previous.next current.next } var zero T current.value zero current.next nil list.size-- return true } previous current current current.next } return false } func (list *LinkedList[T]) Values() []T { values : make([]T, 0, list.size) for current : list.head; current ! nil; current current.next { values append(values, current.value) } return values } func (list *LinkedList[T]) Len() int { return list.size } func main() { var list LinkedList[int] list.PushBack(10) list.PushBack(20) list.PushFront(5) fmt.Println(初始链表:, list.Values()) fmt.Println(删除 10:, list.RemoveFirst(func(value int) bool { return value 10 })) fmt.Println(删除后:, list.Values()) fmt.Println(长度:, list.Len()) }运行结果初始链表: [5 10 20] 删除 10: true 删除后: [5 20] 长度: 2链表和切片的选择操作场景更适合的结构按下标随机读取切片连续存储和缓存友好切片尾部追加切片或带 tail 的链表频繁在头部插入删除链表或双端队列需要保存节点引用链表简单、高效、易维护切片链表不天然比切片快。链表节点分散在内存中遍历缓存局部性较差每个节点还需要额外指针。只有在插入删除位置已经通过节点定位、且不需要随机访问时链表才更有优势。常见错误和避坑提醒误区一解引用 nil 指针var pointer *int // fmt.Println(*pointer) // panic使用前判断 nil或者明确函数契约不接收 nil。误区二以为普通指针可以做地址加减Go 普通指针不能进行指针算术。底层转换需要unsafe.Pointer并且必须严格遵守官方规则。误区三以为指针需要手动释放Go 使用垃圾回收不需要手动free。重点是避免无意义地长期保存引用。误区四传递指针却修改了错误层级**int解引用一次得到*int解引用两次才得到int。遇到多级指针应先明确每一层保存的是什么。误区五所有切片都传指针切片已经包含底层数组引用。修改元素通常不需要*[]T需要改变长度时优先返回新切片。误区六混淆指针接收者和接口实现指针接收者方法通常只属于*T的方法集赋值给接口前要确认具体类型。误区七认为局部变量地址不能返回Go 的逃逸分析会保证返回指针有效。不要把 C 语言中关于栈变量地址的规则直接套用到 Go。一份可以直接复制的综合示例pointer_demo.gopackage main import fmt type Counter struct { value int } func (c *Counter) Increment() { c.value } func (c Counter) Value() int { return c.value } func add(numbers *[]int, value int) { *numbers append(*numbers, value) } func main() { counter : Counter{} counter.Increment() counter.Increment() numbers : []int{1, 2} add(numbers, 3) fmt.Println(counter:, counter.Value()) fmt.Println(numbers:, numbers) }运行结果counter: 2 numbers: [1 2 3]这个示例说明指针接收者可以修改结构体可寻址变量调用指针方法时编译器会自动取地址切片指针可以修改切片描述符普通情况下返回新切片往往比传递*[]T更清晰。总结本章我们学习了 Go 指针的完整基础指针保存变量地址类型写成*T用于取地址*用于解引用未初始化指针的零值是nil解引用 nil 指针会触发运行时 panic指针参数可以让函数修改调用者的变量Go 允许安全返回局部变量指针结构体经常使用指针接收者实现可修改方法new(T)返回指向零值变量的*Tmake用于初始化切片、map 和 channel切片和 map 已经拥有引用语义不应该无条件再套一层指针Go 使用垃圾回收不需要手动释放普通指针指向的内存unsafe.Pointer只适合严格受控的底层场景指针可以实现链表、树和其他节点型数据结构。真正理解 Go 指针之后就不会把它简单看成“内存地址变量”。更准确的理解是指针是一种带类型的间接访问关系它让函数可以修改外部变量让方法可以修改接收者也让链表、树等节点型数据结构能够通过地址建立连接。官方资料Go 语言规范Pointer typesGo 语言规范Address operatorsGo 官方文档Effective Go - Allocation with newGo 官方文档Effective Go - Pointers vs. ValuesGo 官方文档unsafe.Pointer
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

编程思想的庖丁解牛 2026/9/25 21:09:30

编程思想的庖丁解牛

总纲 编程思想不是语法,不是API,而是看待问题、拆分问题、组织代码的思维方式。语法是工具,思想是使用工具的方法论。 底层划分两大主线:面向过程、面向对象;在此基础上衍生出函数式、模块化、分层、事件驱动等工程思想…

阅读更多 →
Atlas 300V 24G推理卡实战:从NPU原理到YOLO部署全流程 2026/9/25 21:09:24

Atlas 300V 24G推理卡实战:从NPU原理到YOLO部署全流程

最近后台收到不少朋友在问 Atlas 300V 24G 这张卡,问法也五花八门,最典型的是“它到底是不是运算加速卡”“能不能拿来跑 YOLO”“和平时用的显卡有什么区别”。我手上正好有张 Atlas 300V 24G 在做推理部署,也把 YOLOv5、YOLOv8 都往这张卡上…

阅读更多 →
给AI“建档立卡“:一套面向智能体的数据资产管理体系 2026/9/25 21:09:24

给AI“建档立卡“:一套面向智能体的数据资产管理体系

目录 0.引言:当 AI 成为数据消费者,资产目录得重写 1.资产盘点:企业面向作业、分析、AI的三类数据资产目录 1.1 面向作业的数据资产目录 1.2 面向分析的数据资产目录 1.3 面向 AI 的数据资产目录 2.训练侧:管理支撑 AI 增…

阅读更多 →
HTTP 407 排错实战:CONNECT、认证注入与容器凭据安全 2026/9/25 21:09:24

HTTP 407 排错实战:CONNECT、认证注入与容器凭据安全

HTTP 407 往往被笼统归为“网络错误”,但它实际上给出了非常具体的协商结果:客户端已经到达中间网关,而网关要求代理认证,当前请求没有提供可接受的凭据。继续替换出口或无上限重试,通常不会改变认证协商失败这一事实。…

阅读更多 →
STM32 USB-CAN 2026/9/25 21:09:17

STM32 USB-CAN

#参考链接 https://cloud.tencent.com/developer/article/1835846 #硬件平台 STM32F072C8T6 #PCB 嘉立创免费打样 #按照网上的原理图自己进行了设计 #问题点 1、BOOT0引脚建议加上开关(对地 对 VDD) 2、USB D引脚需要上拉到VDD (阻值4.7K) #供电采用USB…

阅读更多 →
基于NLP的恶意代码静态特征提取与分类检测工具设计与实现(代码+数据库+LW) 2026/9/25 21:09:17

基于NLP的恶意代码静态特征提取与分类检测工具设计与实现(代码+数据库+LW)

摘 要 针对传统恶意代码检测方法难以应对变种恶意代码、深度学习检测模型可解释性差且部署成本高的问题,设计并实现一款基于自然语言处理(NLP)的恶意代码静态检测工具。工具以 PE 文件和脚本文件为检测对象,通过静态分析提取三类…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉