Go cgo 传指针到 C 怎样遵守指针规则
我第一次把 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 值。
Go SQL 事务遇到死锁怎样设计有限重试
- 上一篇
- Go SQL 事务遇到死锁怎样设计有限重试
- 下一篇
- Go plugin 加载时版本不匹配怎样判断
-
- Golang · Go问答 | 1天前 | go · RGBA image ColorModel At
- Go image 怎样读取不同颜色模型的像素
- 291浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go 怎样把 pprof 挂到独立 ServeMux
- 491浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go embed FS 打开不存在资源时怎样保留错误上下文
- 141浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go template.Funcs 怎样在 ParseFS 前注册自定义函数
- 406浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go XML Decoder Strict 关闭后适合什么兼容场景
- 412浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go 怎样读取子进程资源使用量
- 373浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go os/user Current 和 Lookup 怎样选择
- 171浏览 收藏
-
- Golang · Go问答 | 1天前 | 权限 · Go问答 · Go Unix Socket UnixConn 对端凭据 SO_PEERCRED x/sys/unix
- Go Unix socket 怎样读取对端凭据
- 302浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go Unix 数据报套接字怎样处理报文边界
- 336浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go 项目怎样为 HTTP/3 客户端划分可选依赖
- 471浏览 收藏
-
- Golang · Go问答 | 1天前 |
- Go HTTP/2 异常怎样用 GODEBUG 获取线索
- 366浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 486次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 542次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 563次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 519次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 333次使用
-
- Golang使用CGO与Plugin技术运行加载C动态库
- 2022-12-23 196浏览
-
- Go语言怎么实现CGO编程
- 2023-04-17 342浏览
-
- Go map clear 如何降低大 Map 清理成本:容量复用、垃圾回收与基准验证
- 2026-08-24 134浏览
-
- Go bytes.Buffer Grow 怎么估算容量:扩容时机、Len 与 Cap 的验证
- 2026-08-26 234浏览
-
- Go slices.DeleteFunc 删除指针元素后为什么还占内存:尾部清零、GC 与容量验收
- 2026-08-26 396浏览
