1. 背景:为什么 Go 需要 cgo
1.1 Go 生态的三大「C 依赖」痛点
Go 语言在设计上追求「自带标准库、纯静态编译、跨平台可移植」,但现实工程中几乎每个 严肃项目都会撞上三类必须与 C 打交道的场景:
| 痛点 | 典型场景 | 例子 |
|---|---|---|
| 存量 C 库资产复用 | 厂商 SDK 只有 C 接口 | FANUC FOCAS(fwlib)、西门子 snap7、libmodbus、open62541、ONNX Runtime C API |
| 系统级 API 直调 | 标准库未封装的系统调用 | Windows WinAPI、Linux ioctl、驱动 ioctl、POSIX 扩展 |
| 性能热点下沉 | 解释/GC 开销不适合的极热路径 | 高性能编解码、SIMD 运算、锁竞争极低的临界区 |
1.2 Go 与 C 互操作的四条路线对比
| 路线 | 原理 | 延迟/带宽 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| cgo | 编译期嵌入 C 代码,运行时栈切换直调 | 单次调用 ~50-100ns | 中 | 频繁、细粒度、有状态调用(SDK 句柄) |
| 外部进程 exec | 启动子进程 + IPC | ms 级 | 低 | 低频一次性任务(ffmpeg 转码、git 命令) |
| 网络服务化 | C 库包装成 RPC/gRPC 服务 | 亚毫秒~毫秒 | 高 | 多语言共享、C 库有服务化潜力 |
| C 侧反向嵌入 | Go 编译成 c-shared 库被 C 主程序调用 | 同 cgo | 中 | C 主程序 + Go 逻辑(插件化) |
工业数采场景(FANUC/三菱/西门子直连)的 SDK 都是「建立连接句柄 → 轮询读写点位」模式, 调用频繁且有状态,因此 cgo 是唯一现实路线。
2. cgo 核心概念与底层原理
2.1 import "C" 伪包:cgo 的三个角色
import "C" 不是普通包,它触发 Go 工具链中的 cgo 预处理阶段,承担三个角色:
- C 代码注入:import "C" 上方注释块(/* ... */)中的代码会被提取出来,交给 C 编译器编译;
- 符号暴露:C 侧的类型、函数、常量、宏、枚举、全局变量,以 C.xxx 形式暴露给 Go;
- 代码生成:cgo 工具生成 _cgo_gotypes.go(Go 侧桩)、_cgo_export.c(导出桩)、 _cgo_defun.c(汇编跳板)等中间文件,完成两侧的类型转换与调用约定桥接。
2.2 #cgo 指令
在注释块中用 #cgo 指令向构建系统传递 C 编译参数:
// #cgo CFLAGS: -I./include
// #cgo LDFLAGS: -L./lib -lfwlib64 -lm
// #cgo pkg-config: libusb-1.0
import "C"
| 指令 | 作用 |
|---|---|
| CFLAGS | C 编译参数(头文件路径、宏定义、优化级别) |
| CPPFLAGS | 预处理器参数 |
| CXXFLAGS | C++ 编译参数(编译 .cc 文件时用) |
| LDFLAGS | 链接参数(库路径、-l 库名) |
| pkg-config: | 通过 pkg-config 自动展开依赖 |
支持按平台分支:#cgo windows LDFLAGS: -lws2_32、#cgo linux CFLAGS: -D_GNU_SOURCE、 #cgo darwin CFLAGS: -I/usr/local/include。
2.3 C 与 C++:cgo 只认 C
cgo 只能直接调用 C(及 C ABI)代码,不能直接解析 C++。C++ 类、重载、命名空间、 模板无法被 cgo 识别。封装 C++ 库必须走「薄 C 桥接层」:
cpp
// wrapper.h ------ C++ 库的 C 封装
#ifdef __cplusplus
extern "C" {
#endif
typedef void* FwHandle;
FwHandle fw_open(const char* ip);
int fw_read_macro(FwHandle h, int idx, double* out);
void fw_close(FwHandle h);
#ifdef __cplusplus
}
#endif
cpp
// wrapper.cpp
extern "C" FwHandle fw_open(const char* ip) {
return static_cast<FwHandle>(new FanucSDK(ip));
}
// ...
Go 侧只 import C 的 wrapper.h,完全感知不到 C++。
2.4 cgo 的线程模型:M 锁定
cgo 调用发生在 goroutine 绑定到 OS 线程(M) 的上下文中。Go runtime 在进入 C 代码前 会将当前 goroutine 对应的 M 锁定(runtime.lockOSThread 语义),保证 C 调用期间不会被 调度器迁移线程;C 代码返回后才解锁。这意味着:
- C 库的线程亲和性(如 TLS 依赖、线程局部状态)在单次调用内是安全的;
- 但多个 goroutine 并发调用同一个 C 库,本质是多个不同 OS 线程同时进入 C 库------ C 库内部若非线程安全(大多数工业 SDK 是),必须自行加锁串行化;
- C 侧阻塞(如 recv 阻塞)会占住一个 M,大量并发阻塞 C 调用会耗尽线程池导致 goroutine 饥饿。
3. 核心 API 说明与速查表
3.1 类型与符号引用
| Go 写法 | 含义 |
|---|---|
| C.int C.long C.size_t | C 基本类型别名 |
| C.struct_fwdata | C 结构体类型 |
| C.fwdata | C typedef 类型 |
| C.fw_open | C 函数 |
| C.MACRO_MAX | C 宏常量(必须是整数/字符串字面量) |
| C.ENUM_VAL | C 枚举值 |
| C.union_xxx | C 联合体(字段访问受限) |
| C.char 数组字段 | 结构体中的 char bufN |
3.2 字符串与字节数组转换
| 函数 | 方向 | 内存来源 | 必须释放 |
|---|---|---|---|
| C.CString(s) | Go string → C char* | C 堆(malloc) | 必须 C.free |
| C.CBytes(b) | Go \[\]byte → C void* | C 堆(malloc) | 必须 C.free |
| C.GoString(p) | C char* → Go string | 拷贝,无需释放 | 否 |
| C.GoStringN(p, n) | C char* 定长 → Go string | 拷贝 | 否 |
| C.GoBytes(p, n) | C void* 定长 → Go \[\]byte | 拷贝 | 否 |
关键语义 :Go → C 方向是拷贝 (分配到 C 堆);C → Go 方向也是拷贝(分配到 Go 堆)。不存在零拷贝互转。想零拷贝请走 unsafe.Pointer(见 3.4)。
3.3 Go 指针传递规则(最重要的安全约束)
Go 1.6 起,cgo 调用 C 代码时,Go 侧传给 C 的指针不得指向 Go 管理的堆内存, 除非该内存「不包含 Go 指针」。违反此规则会导致运行时 cgo argument has Go pointer to Go pointer 崩溃。
简化规则:
- 允许:C.CString / C.CBytes 分配的 C 堆内存指针;
- 允许 :指向不含 Go 指针的 Go 内存 (如 \[\]byte 底层数组、unsafe 切片), 但 C 侧不得在调用返回后继续持有(除非满足 4 中例外);
- 禁止:把包含 Go 指针的结构体(如 \[\]interface{}、map、带 string 字段的 struct)传给 C;
- 例外://go:cgo_export_dynamic 配合 runtime.KeepAlive + 线程绑定可让 C 长期持有,但极不推荐新手使用;
- 推荐模式 :C 侧需要长期保存状态时,用 cgo.Handle(见 3.5)。
3.4 零拷贝 unsafe.Pointer
需要把 Go 的 \[\]byte 零拷贝传给 C 时(且 C 只在本次调用内使用):
Go
p := unsafe.Pointer(&buf[0]) // buf 为 []byte,非空
C.consume(p, C.int(len(buf)))
约束:buf 必须非空、C 不得持有指针跨调用、调用期间 runtime.KeepAlive(buf) 防止 GC 回收。
3.5 cgo.Handle:C 侧安全持有 Go 对象
Go 1.17 引入 runtime/cgo.Handle,是「C 侧存 Go 对象」的官方安全机制:
Go
h := cgo.NewHandle(obj) // 得到 uintptr 句柄
C.set_callback_data(h) // 把句柄传给 C 长期保存
// ... C 侧回调时通过 h 取回 Go 对象
obj2 := h.Value().(MyType)
h.Delete() // 用完后显式释放
C 侧只保存一个 uintptr_t,永不直接接触 Go 内存;Handle 有引用计数与防重复 Delete 检查。
3.6 回调与导出:C → Go 方向
C 回调 Go 函数需要两件事:
Go
//export onData
func onData(handle C.int, data *C.char, len C.int) {
// 在 C 线程上执行;禁止调用会阻塞或操作 Go 调度器的重操作
}
回调在 C 创建的线程上执行(非 goroutine),以下事项必须注意:
- 回调期间不能调用 runtime.LockOSThread 之外的操作来改变线程状态;
- 回调中创建 goroutine 是允许的,但 goroutine 真正调度发生在返回 Go 侧之后;
- 回调中调用 fmt.Println 等会触发 GC/调度器的操作性能很差,高频回调应只做 「投递事件到 channel / 写入环形缓冲」等轻量动作。
3.7 errno 与错误处理
C 库用 errno 报告错误时,cgo 自动捕获 errno 并在 Go 侧通过 C.errno 或 syscall.Errno 语义暴露(Go 1.x 的 C 伪包会在调用后把 errno 存到 goroutine 局部, 可用 C.C.errno 读取)。工业 SDK 更常见的是返回码体系(如 FOCAS 的 EW_OK), 应在 Go 侧建立「返回码 → 错误对象」映射表。
4. 详细使用说明(8 个可编译示例)
4.1 示例 1:最小 cgo------调用 C 函数
Go
package main
/*
#include <math.h>
#include <stdio.h>
static void hello(const char* name) {
printf("hello %s from C\n", name);
}
*/
import "C"
import "fmt"
func main() {
C.hello(C.CString("go")) // 注意:C.CString 未 free,见坑 1
fmt.Println("sqrt(2) =", C.sqrt(2.0))
}
4.2 示例 2:字符串转换的正确释放模式
Go
package main
/*
#include <stdlib.h>
#include <string.h>
static char* dup(const char* s) {
char* out = strdup(s);
return out;
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func callDup(s string) string {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs)) // 必须释放
out := C.dup(cs) // C 侧 strdup 产生新 C 内存
defer C.free(unsafe.Pointer(out)) // 也必须释放
return C.GoString(out)
}
func main() {
fmt.Println(callDup("工业数采"))
}
4.3 示例 3:调用 FANUC FOCAS fwlib
Go
package main
/*
#cgo LDFLAGS: -L./lib -lfwlib64
#include "fwlib32.h"
*/
import "C"
import (
"fmt"
"unsafe"
)
type FanucConn struct {
h C.ushort // FOCAS 连接句柄
}
// Connect 建立以太网连接(FOCAS1,端口 8193)
func Connect(ip string, port int) (*FanucConn, error) {
cip := C.CString(ip)
defer C.free(unsafe.Pointer(cip))
var h C.ushort
ret := C.cnc_allclibhndl3(cip, C.ushort(port), C.short(10), &h)
if ret != C.EW_OK {
return nil, fmt.Errorf("focas connect failed: %d", ret)
}
return &FanucConn{h: h}, nil
}
// ReadMacro 读取宏变量 #100 系列
func (f *FanucConn) ReadMacro(no int) (float64, error) {
var val C.double
ret := C.cnc_rdmacro(f.h, C.short(no), 1, &val)
if ret != C.EW_OK {
return 0, fmt.Errorf("read macro %d failed: %d", no, ret)
}
return float64(val), nil
}
func (f *FanucConn) Close() {
C.cnc_freehndl(f.h)
}
要点:fwlib 是 C 库,连接句柄 ushort 在 Go 侧作为不透明句柄长期持有,绝不可 把 Go 结构体指针传给 C 保存(违反指针规则),句柄值传递则完全安全。
4.4 示例 4:结构体指针传递(modbus 寄存器批量读写)
Go
package main
/*
#include <stdint.h>
#include <string.h>
typedef struct {
uint16_t addr;
uint16_t count;
uint16_t values[125];
} RegBatch;
static int batch_read(RegBatch* b) {
for (int i = 0; i < b->count; i++) {
b->values[i] = b->addr + (uint16_t)i; // 模拟读回
}
return 0;
}
*/
import "C"
import "unsafe"
type RegBatch struct {
Addr uint16
Count uint16
Values [125]uint16
}
func main() {
b := &RegBatch{Addr: 100, Count: 10}
// Go 结构体内存不含 Go 指针 → 允许直接传指针
ret := C.batch_read((*C.RegBatch)(unsafe.Pointer(b)))
_ = ret
}
注意:Go 结构体与 C 结构体布局需一致(字段顺序、宽度、对齐)。工业 SDK 常带 #pragma pack(1),Go 侧需用 unsafe + 手写偏移或 struct{} 对齐技巧保持兼容。
4.5 示例 5:C 回调调用 Go(事件/告警模式)
Go
package main
/*
#include <stdint.h>
typedef void (*event_cb)(int32_t code, const char* msg);
static void register_cb(event_cb cb) {
// 模拟 C 侧在收到事件时回调
cb(1001, "spindle overload");
}
*/
import "C"
import (
"fmt"
"runtime/cgo"
"unsafe"
)
//export onEvent
func onEvent(code C.int32_t, msg *C.char) {
// C 线程上轻量执行:只投递事件
fmt.Printf("event %d: %s\n", int(code), C.GoString(msg))
}
func main() {
C.register_cb(C.event_cb(C.onEvent)) // 导出函数地址传给 C
select {} // 阻塞保持进程存活
}
4.6 示例 6:cgo.Handle 让 C 长期持有 Go 对象
Go
package main
/*
#include <stdint.h>
#include <stdlib.h>
static void* saved;
static void save(uintptr_t h) { saved = (void*)h; }
static uintptr_t load() { return (uintptr_t)saved; }
*/
import "C"
import (
"fmt"
"runtime/cgo"
)
type Worker struct {
id int
}
func main() {
w := &Worker{id: 42}
h := cgo.NewHandle(w) // C 侧只保存句柄,不碰 Go 内存
C.save(C.uintptr_t(h))
defer h.Delete()
h2 := cgo.Handle(C.load())
fmt.Println(h2.Value().(*Worker).id) // 42
}
4.7 示例 7:Windows 上调用系统 DLL(syscall 与 cgo 对比)
cgo 更「原生」但要求本机装有 C 工具链;纯 Go 的 syscall.NewLazyDLL 免工具链:
Go
// 方式 A:cgo(需 gcc 环境)
// #cgo windows LDFLAGS: -luser32
// C.MessageBoxW(nil, ...)
// 方式 B:syscall.NewLazyDLL(纯 Go,推荐)
package main
import "syscall"
var (
user32 = syscall.NewLazyDLL("user32.dll")
msgBoxW = user32.NewProc("MessageBoxW")
)
func main() {
msgBoxW.Call(0,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("hi"))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("title"))), 0)
}
4.8 示例 8:构建约束与平台差异化
Go
//go:build linux && cgo
// +build linux,cgo
package main
/*
#cgo linux LDFLAGS: -lrt
#include <time.h>
*/
import "C"
生产工程建议用构建标签把 cgo 代码隔离到独立文件,纯 Go 文件作为同接口 fallback (CGO_ENABLED=0 时仍能编译)。
5. 底层实现剖析:cgo 的运行时机制
5.1 调用链全景

每次调用经历:Go 栈 → C 栈切换 → entersyscall/exitsyscall 与调度器交互, 这是 cgo 调用「贵」的根源------空调用基准约 40~100ns(Go 普通函数 ~1ns)。
5.2 为什么「Go 指针不能传给 C」
Go 的 GC 是移动式 (指 GC 会搬动对象)吗?不,Go GC 是非移动的,但指针信息 跟踪是精确的 :GC 扫描时需要知道每个对象里哪些字段是指针。C 代码是 GC 盲区------ C 持有的 Go 指针一旦发生 GC 扫描,Go runtime 无法判断 C 是否仍在引用它,可能发生 use-after-free(对象被回收而 C 还在用) 或 GC 扫描到 C 栈上的伪造指针导致 误报。因此运行时强制「传给 C 的内存不得含 Go 指针」。
5.3 errno 的线程安全捕获
cgo 在进入 C 前保存 errno 到 goroutine 局部,C 返回后恢复并记录到 C._errno,避免多线程 C 调用互相污染 errno。
5.4 CGO_ENABLED 与静态链接
- CGO_ENABLED=1(默认):允许 cgo;链接器使用 C runtime,产出依赖 glibc 的动态链接;
- CGO_ENABLED=0:纯 Go,静态链接,跨平台分发友好;代价是无法用 cgo。
- 交叉编译(GOOS=linux GOARCH=arm64 等)时 cgo 需要目标平台交叉 C 工具链,通常 CGO_ENABLED=0 是首选。
6. 常错点 22 条
| 坑 | 症状 | 正确做法 | |
|---|---|---|---|
| 1 | C.CString 忘 C.free | 每次调用泄漏 C 堆内存,长期运行 OOM | 一律 defer C.free(unsafe.Pointer(cs)) |
| 2 | 把含 Go 指针的结构体传给 C | 运行时 cgo argument has Go pointer to Go pointer panic | 用 cgo.Handle 或 C 侧 malloc 拷贝 |
| 3 | C 侧长期持有 Go 切片指针 | GC 回收后 C 侧 use-after-free,偶发崩溃 | 只在调用内使用 + runtime.KeepAlive,或 cgo.Handle |
| 4 | cgo 直接 import C++ 头文件 | 编译报错(C++ 语法不识别) | 用 extern "C" 薄桥接层 |
| 5 | 回调中做重活(日志/同步 I/O) | C 线程卡死、事件丢失、性能骤降 | 回调只投递事件,返回后再处理 |
| 6 | 多个 goroutine 并发调非线程安全 C 库 | 数据竞争、句柄损坏、随机崩溃 | 包级互斥锁串行化 |
| 7 | C 侧阻塞调用占满 M | goroutine 饥饿、程序假死 | 限制并发(信号量)+ 超时机制 |
| 8 | 结构体布局/对齐不一致(#pragma pack) | 字段错位、读回脏数据 | 按 C 头文件精确复刻布局 + 单元测试验证 |
| 9 | C.struct_x 字段名大小写错误 | 编译期 unknown field | 字段名必须与 C 头文件一致(含下划线) |
| 10 | 忘 //export 声明回调 | 编译报 could not determine kind of name for C.onEvent | 导出函数必须在 import "C" 同文件 + //export |
| 11 | 忘 defer C.free C 侧 strdup/malloc 结果 | C 堆泄漏 | C 侧内存一律显式释放 |
| 12 | 链接顺序错误(-l 在源文件前) | 链接器 undefined reference | LDFLAGS 中 -l 放在库路径后;依赖库按依赖序排列 |
| 13 | 静态库与动态库混用 | 链接失败或运行时缺 .so/.dll | 明确库类型,动态库放入 PATH/LD_LIBRARY_PATH |
| 14 | 忽略 C 返回码 | 静默失败、数据无效不报错 | 每个 C 调用检查返回码并映射为 Go error |
| 15 | 回调中调用 Go 侧重函数触发 GC 卡顿 | 高频回调下整体延迟飙升 | 回调内零分配、零锁 |
| 16 | //export 与普通 Go 函数重名 | 链接冲突 | 导出名加前缀(如 export_OnEvent) |
| 17 | 交叉编译忘关 CGO | 缺交叉编译器报错 | CGO_ENABLED=0 go build |
| 18 | 大数组逐字段传给 C | 调用次数爆炸、开销巨大 | 整块指针 + 长度参数 |
| 19 | 用 cgo 做热路径高频小调用 | 性能掉一个数量级 | 批量接口/降频/纯 Go 重写 |
| 20 | errno 与返回码混用 | 误判错误来源 | 明确库的错误契约(返回码 or errno) |
| 21 | C 代码 panic(段错误) | 整个进程崩溃,无法 recover | C 侧防御性检查;Go 侧无法兜底 |
| 22 | 句柄生命周期管理混乱(重复 close / 用后 close) | C 库未定义行为 | Go 侧封装成对象 + 幂等 Close |
7. 性能优化清单 12 条
- 批量代替细粒度:FOCAS 用 cnc_rdmacro 批量读、snap7 用 ReadMultiVars,一次 cgo 调用读取 N 个点位,把调用次数从 N 降到 1;
- 连接句柄长生命周期:连接/断开一次,长期持有句柄轮询,避免反复握手;
- 缓存字符串转换:连接 IP、点位名等常量字符串预转 C.CString 并缓存复用 (注意线程安全与释放策略);
- 回调降频 + 批量投递:C 回调事件先写入环形缓冲/无锁队列,Go 侧批量消费;
- 限制并发 C 调用:信号量限制同时进入 C 的 goroutine 数,保护 M 线程池;
- 避免回调内触发 GC:回调内零分配,事件用预分配缓冲;
- CGO 调用开销量化:先用 benchmark 测量(空调用 ~50ns、复杂调用更大),再决定 优化方向;
- 优先纯 Go 实现:像三菱 MC 协议那样能纯 Go 自实现就纯 Go,彻底消除切换开销;
- 减少数据拷贝:unsafe.Pointer 零拷贝传 \[\]byte(遵守指针规则);
- pprof 观测:go tool pprof CPU profile 中 cgo 调用显示为 cgo* 符号, 可用于定位热点;
- C 侧编译优化:CFLAGS: -O2,让 C 代码本身高效;
- 优雅降级:CGO_ENABLED=0 的 fallback 实现(如连接诊断工具)保证无 C 环境 也能编译测试。
基准参考
BenchmarkGoCall 1.2 ns/op
BenchmarkCGoEmptyCall 58.3 ns/op (约 50 倍差距)
BenchmarkCGoStrConv 210.0 ns/op (含 C.CString + GoString + free)
结论:单次 cgo 调用开销可接受(~百 ns 级),但高频(>1M/s)或热循环内必须规避。
8. 总结:适用场景、选型决策树与 FAQ
8.1 适用场景表
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| FANUC fwlib / snap7 / 厂商 C SDK 轮询采集 | cgo 封装 | SDK 只提供 C 接口、有状态句柄 |
| 简单系统调用(getpid/ioctl) | syscall 包 | 免 C 工具链、开销更低 |
| 低频一次性外部工具(ffmpeg/git) | exec.Command | 进程隔离、无需 C 依赖 |
| 热路径编解码 | 纯 Go 重写 / SIMD 汇编 | cgo 切换开销不可接受 |
| 跨平台分发 | CGO_ENABLED=0 纯 Go + fallback | 静态链接、免 glibc 依赖 |
| C 主程序 + Go 插件 | -buildmode=c-shared | 反向嵌入 |
8.2 选型决策树
需要调用 C/C++ 能力?
├─ 能纯 Go 实现且性能达标 ──► 纯 Go(最优先,如三菱 MC 篇)
├─ 简单系统调用 ──► syscall / golang.org/x/sys
├─ 低频一次性外部工具 ──► os/exec
├─ SDK 有状态句柄、频繁调用 ──► cgo(本文主线)
└─ 需要被 C 主程序调用 ──► -buildmode=c-shared 反向嵌入
8.3 FAQ 10 条
| 问题 | 答案 |
|---|---|
| cgo 调用开销到底多大? | 空调用约 50-100ns,是 Go 函数调用的 ~50 倍;批量设计可摊薄 |
| 为什么不能把 Go 指针长期传给 C? | GC 精确扫描与 C 盲区冲突,可能导致 use-after-free |
| 如何封装 C++ 库? | 写 extern "C" 薄 C 桥接层,cgo 只认 C ABI |
| 回调在哪个线程执行? | C 创建的线程,非 goroutine;只做轻量投递 |
| Windows 上必须装 gcc 才能用 cgo? | 是的(MinGW);纯系统调用可用 syscall.NewLazyDLL 免工具链 |
| CGO_ENABLED=0 时 import "C" 文件会怎样? | 编译报错,需用 build tag 隔离 fallback |
| 多个 goroutine 同时调 C SDK 安全吗? | 取决于 C 库线程安全;工业 SDK 大多非线程安全,需加锁 |
| 如何读 C 结构体中的 char buf64? | C.GoBytes(unsafe.Pointer(&s.buf0), 64) 后按需处理 |
| cgo 会不会拖慢 GC? | 调用切换有 entersyscall/exitsyscall 开销;回调内分配会加剧 |
| 生产采集网关如何组织 cgo 代码? | SDK 封装层独立包 + 接口抽象 + 纯 Go fallback + 集成测试 |
10. 参考资料
- Go 官方文档:Command cgo(go.dev/cmd/cgo)
- Go 官方 Wiki:cgo(含指针规则详解)
- runtime/cgo 包文档(Handle 机制)
- FANUC FOCAS2 Library(fwlib)开发者手册
- snap7 / gos7 源码