Go cgo 传指针到 C 怎样遵守指针规则
来源:17golang原创
时间:2026-10-14 23:23:07 124浏览 收藏
我第一次把 Go 的字节缓冲区交给 cgo 时,真正难排查的不是编译错误,而是 C 函数“当场能读、返回后又想继续用”的边界。判断能不能传指针,不能只看它是不是 unsafe.Pointer:要看这块内存由谁分配、里面是否还藏着 Go 指针,以及 C 是否会把地址保存到调用之外。
最稳妥的规则是:Go 指针可以在一次 C 调用期间指向已固定、且不包含未固定 Go 指针的内存;C 不能在调用返回后继续保存它。需要跨调用保存时,复制到 C 堆,或者使用 runtime/cgo.Handle 管理 Go 值。
先按内存来源判断,而不是按指针类型猜
cgo 文档把 Go 指针定义为指向 Go 分配内存的指针,把 C 指针定义为指向 C 分配内存的指针。这个属性由分配方式决定,与变量写成什么指针类型无关。
所以,*byte 可能指向 Go 堆,也可能是从 C.malloc 得到的 C 内存。接口设计时我会先画出两条边界:Go 堆由 Go 垃圾回收器管理,C 堆由 C 的释放约定管理。下面这张图只描述这条内存关系,是静态说明图,不是运行截图。

这也解释了为什么“把参数转成 unsafe.Pointer”不是放宽规则的开关。它只改变了表达方式,不改变内存的归属和生命周期。
调用期间可以怎样借用 Go 内存
如果 C 函数只在调用期间读取一段不包含 Go 指针的连续数据,可以把指向这段内存的 Go 指针传进去。函数参数指向的 Go 内存会在调用期间被隐式固定,但 C 必须在函数返回前完成访问。
package main /* #include// C 函数只在本次调用中读取字节,不把指针保存到全局或结构体。 static void consume_bytes(const unsigned char *data, size_t n) { for (size_t i = 0; i
空切片不能取 &buf[0];切片的整块 backing array 都属于这次传递所涉及的 Go 内存。若结构体字段、切片或字符串内部还带有 Go 指针,就不能把它们当成普通无指针字节块传入。
C 保存指针时,问题从调用变成了生命周期
最危险的接口通常不是当场崩溃,而是 C 结构体里有一个字段,函数返回后还保留着刚才收到的 Go 地址。字符串、切片、channel、map、interface 和函数值都不能按“地址留着以后再用”的方式交给 C;它们包含 Go 指针,且常规情况下不能用 runtime.Pinner 固定。

如果 C 只需要字节,通常复制最直观:
package main /* #include// C 侧接管这份副本的生命周期,调用者必须最终调用 free_copy。 static void use_copy(const unsigned char *data, size_t n) { for (size_t i = 0; i
复制方案的代价是一次分配和一次拷贝,但所有权容易写进接口:谁分配,谁释放,C 保存多久也不会影响 Go 堆。
确实要让 C 持有 Go 内存时,再考虑 Pinner
runtime.Pinner 可以管理某些 Go 内存的固定期限,但它不是“任何 Go 对象都能永久传给 C”的通行证。被传递的内存不能包含未固定的 Go 指针,C 保存副本的整个期间都要保持固定;固定一次就必须对应一次解固定。
package main
import (
"runtime"
"unsafe"
)
// passPinned 展示固定期限管理,真实接口仍必须确认 C 不会超出约定。
func passPinned(buf []byte, cCall func(unsafe.Pointer, uintptr)) {
if len(buf) == 0 {
return // 没有首地址时不进入固定流程。
}
var pinner runtime.Pinner
pinner.Pin(&buf[0])
defer pinner.Unpin() // 与 Pin 成对,避免固定状态泄漏。
// cCall 必须把保存期限限制在这次固定区间内。
cCall(unsafe.Pointer(&buf[0]), uintptr(len(buf)))
}
对普通字节数据,我通常优先选择 C 堆复制;Pinner 适合边界明确、确实需要让 C 访问同一块 Go 内存的场景。
长期保存 Go 值时用 cgo.Handle,而不是保存地址
runtime/cgo.Handle 把 Go 值登记到句柄表中,传给 C 的是一个整数句柄。C 可以保存这个句柄,回调 Go 时再把它转换回原值;使用结束后必须删除句柄,否则 Go 值仍会被句柄表引用。
package main /* #include// C 侧保存的是整数句柄,不是 Go 对象地址。 static uintptr_t saved_handle; static void save_handle(uintptr_t h) { saved_handle = h; } static uintptr_t load_handle(void) { return saved_handle; } */ import "C" import ( "fmt" "runtime/cgo" ) func registerValue(v string) cgo.Handle { h := cgo.NewHandle(v) C.save_handle(C.uintptr_t(h)) // 只跨语言传递可识别的句柄值。 return h } func readAndRelease() { h := cgo.Handle(C.load_handle()) value := h.Value().(string) // 使用前保证句柄仍然有效且类型匹配。 fmt.Println(value) h.Delete() // C 不再回调后及时删除,释放句柄表中的 Go 值。 }
句柄同样需要生命周期协议:C 保存句柄期间不能调用 Delete,而 C 丢弃句柄后必须通知 Go 清理。它解决的是“如何安全引用 Go 值”,不是让 C 获得一个可以随意解引用的 Go 指针。
把检查和失败处理写进 cgo 接口契约
cgo 会动态检查一部分指针传递问题。默认的 GODEBUG=cgocheck=1 提供成本较低的检查;更严格的检查可以在构建时使用 GOEXPERIMENT=cgocheck2。检查发现问题时,应回到接口设计确认 C 是否保存了指针、传入对象是否包含 Go 指针,而不是直接关闭检查。
| 问题 | 可以继续传指针吗 | 更稳的处理 |
|---|---|---|
| C 是否只在函数返回前访问 | 是,才进入临时借用路径 | 把期限写进注释和 C 接口契约 |
| 传入内存是否包含 Go 指针 | 包含时不要当作普通字节块传递 | 复制成 C 数据或拆成无指针字段 |
| C 是否要异步保存 | 不能直接保存普通 Go 指针 | 复制到 C 堆,或保存 cgo.Handle |
| 是否使用 Pinner | 只有固定对象和期限都清晰时 | Pin/Unpin 严格成对,保留释放责任 |
不要用 GODEBUG=cgocheck=0 把异常变成“正常运行”。绕过约束可能导致不可预测的崩溃或内存破坏;禁用检查最多只能用于定位阶段,不是接口修复。
最后的判断口诀
我现在会把 cgo 指针问题压缩成四句话:谁分配,谁负责;只借用,不留地址;有 Go 指针就别伪装成字节;跨调用保存用 C 副本或句柄。只有在固定范围和释放责任都写清楚时,才让 Pinner 参与。
如果你刚遇到 cgo argument has Go pointer to unpinned Go pointer 一类错误,先查 C 是否保存了参数,再查传入结构内部是否含有 slice、string、map、interface 或函数值。通常把接口改成“调用内完成”或“C 堆拥有副本”,比继续增加 unsafe.Pointer 转换更接近根因。
相关问题
Go 的 slice 能直接传给 C 吗?
可以在满足 cgo 指针规则且 C 只在调用期间使用时传递底层连续元素;空 slice 要先处理,C 不能把地址保存到调用返回之后。
为什么 C 不能长期保存 Go string 的地址?
字符串内部带有 Go 指针,常规方式不能用 Pinner 把它固定成可长期保存的 C 数据。需要长期使用时复制成 C 字符串或改用句柄表达 Go 值。
runtime/cgo.Handle 会自动释放吗?
不会。创建句柄后,必须在 C 不再使用它时显式调用 Delete,否则句柄表仍会持有对应的 Go 值。
-
196 收藏
-
342 收藏
-
134 收藏
-
234 收藏
-
396 收藏
-
302 收藏
-
336 收藏
-
471 收藏
-
366 收藏
-
122 收藏
-
331 收藏
-
186 收藏
-
485 收藏
-
441 收藏
-
330 收藏
-
270 收藏
-
337 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习