本文详解如何通过 cgo 将 c 函数返回的 `struct person*` 数组及其长度安全转换为 go 切片,并避免内存泄漏或越界访问。核心在于利用 `unsafe.slice`(go 1.17+)或传统 `(*[n]t)(unsafe.pointer(p))[:len:len]` 惯用法,配合显式内存管理。
在 CGO 编程中,C 函数常以指针 + 长度方式返回动态分配的结构体数组(如 struct Person* get_team(int *n)),而 Go 原生不支持直接操作 C 数组。要安全、高效地将其转为 Go 可用的切片,需结合 unsafe 包与明确的生命周期控制。
假设 C 端定义如下:
// person.h
struct Person {
char* name;
int age;
};
struct Person* get_team(int* n);对应的 Go 调用应严格遵循以下步骤:
package main
/*
#include "person.h"
*/
import "C"
import (
"fmt"
"unsafe"
)
func getTeam() []C.struct_Person {
var n C.int = 0
teamPtr := C.get_team(&n)
if teamPtr == nil {
return nil
}
defer C.free(unsafe.Pointer(teamPtr)) // ⚠️ 注意:defer 在函数返回时才执行!
// 安全转换:unsafe.Slice 是类型安全、无 panic 风险的首选
teamSlice := unsafe.Slice(teamPtr, int(n))
return teamSlice
}
// 使用示例
func main() {
team := getTeam()
for i, p := range team {
// 注意:C 字符串需手动转 Go 字符串(如 C.GoString(p.name))
fmt.Printf("Person %d: age=%d\n", i, int(p.age))
}
// team 切片在此处仍有效 —— 因为 C.free 尚未触发(defer 在 getTeam 返回时才执行)
}若使用较老版本,可沿用经典惯用法(原理相同,但需指定“足够大”的数组长度):
teamSlice := (*[1 << 30]C.struct_Person)(unsafe.Pointer(teamPtr))[:int(n):int(n)]
该写法本质是:将原始指针强制解释为超大数组的首地址,再切出长度为 n、容量也为 n 的子切片。1

func bad() []C.struct_Person {
var n C.int
p := C.get_team(&n)
defer C.free(unsafe.Pointer(p)) // ❌ defer 在函数末尾执行,但切片已返回!
return (*[1 << 30]C.struct_Person)(unsafe.Pointer(p))[:int(n):int(n)]
}此时调用方拿到的切片底层内存可能已被释放,导致 undefined behavior(崩溃或脏数据)。
| 项目 | 推荐方案 |
|---|---|
| 切片转换 | Go 1.17+ 优先用 unsafe.Slice(ptr, len);旧版用 (*[max]T)(ptr)[:len:len] |
| 内存释放 | C.free(unsafe.Pointer(ptr)),且确保在切片使用完毕后执行(通常用 defer 在同一作用域) |
| 字符串处理 | 对 *C.char 字段调用 C.GoString() 获取安全副本 |
| 跨函数传递 | 不传递原始切片,而是复制所需字段到 Go 结构体中 |
通过以上方法,你既能高效复用 C 层的内存布局,又能保持 Go 代码的可维护性与安全性——前提是你始终牢记:CGO 是桥梁,而非屏障;内存责任,仍在开发者肩上。