让 Go 走得更远:用 LLGo 打通 Go 与 C/C++/Python 生态

本文整理自七牛云基础架构开发工程师、XGo / LLGo / LLAR 项目贡献者郭浩蓝(@meteorsliu)于 2026 年 11 月 7 日在上海 Google DevFest 现场发表的主题演讲,并结合现场演示 PPT 补充了相关代码与实测数据。

我们还需要关注编程语言吗?

1

在讨论编译器与底层生态之前,我们可以先思考一个问题:进入 AI 时代,程序员们还有必要关注编程语言本身吗?

70 年前的 1957 年,IBM 发布了 Fortran 语言,推动程序员从“手写汇编时代”迈入“高级编程语言时代”。今天的技术演进与此非常相似,AI 正将我们从“手写高级语言代码”带入“AI 辅助编程”的新阶段。

正如高级语言的编译器无法覆盖所有底层硬件场景、部分核心逻辑依然需要人工编写汇编代码一样,在今天的复杂工程实践中,AI 对很多深层业务与底层架构的理解依然不如人类透彻。工程师们需要高频阅读、理解、审查代码,并引导 AI 生成符合预期的逻辑。

这也决定了 AI 时代对理想编程语言的核心诉求:可读性强、设计简洁、逻辑严密,同时具备丰富的生态覆盖。

Go 语言在“简洁性”上无疑是一个可靠载体。业内有个调侃:在 AI 时代,Java 程序员最常遇到的报错可能就是 "You have reached your weekly usage limit"(你的周额度用完了);Rust 程序员大概率也逃不过额度耗尽,还得面对编译器抛出的一大堆告警。所以,AI 时代可能 Java 程序员比我们这些 Go nerds 更喜欢 Go,因为比较省 Token。Rust 程序员往往不说喜欢 Go,他们比较嘴硬,老说 Rust 是世界上最好的语言——当然,我觉得他们的编译器可能不太同意。

当然,谈笑归谈笑,回到工程现实中,XGo 团队的目标是让每一个开发者都能简单、轻松地写出自己的程序。而要实现这一点,就必须直面 Go 的边界,把过往几十年沉淀的异构技术生态连接起来。

Go 走出服务端的痛点

Go 在云原生与服务端开发中有着广泛的应用,但这只是现代软件工程的一部分。当我们将视野拓展到更多场景时,Go 依然面临互操作与平台挑战:

  • C/C++ 底层生态复用受限:C 和 C++ 积累了数十年的底层基础设施与核心库资产,桌面软件、操作系统内核以及各类底层驱动都高度依赖 C 生态。
  • 嵌入式与 WebAssembly 的体积约束:微控制器等资源受限设备需要更轻量的运行时,浏览器等 WebAssembly 环境也需要更轻量的构建产物,而标准 Go 编译器自带完整的 Runtime,构建产物往往偏大。
  • 在 Go 中复用 Python 库:Python 聚集了当前主流的 AI 模型、数据科学库与工具链,Go 很难直接、自然地复用 NumPy、PyTorch 等生态资产。

针对 C 生态,Go 官方给出的方案是 cgo。但写过 cgo 的开发者都很清楚它在双向调用、函数回调以及交叉编译上的繁琐程度。

以调用 C 标准库 stdlib.h 中的快速排序函数 qsort 为例,我们需要传入一个自定义的比较函数。在 cgo 中,要完成这一个基础调用,需要先通过 //export 暴露 Go 符号,再编写一段 C 语言的 Wrapper 函数去中转调用:

// cgo 方案:Go 侧导出符号并调用 C 包装函数
//export goCompare
func goCompare(a, b unsafe.Pointer) C.int { ... }

a := []C.int{3, 1, 2}
C.sortInts(&a[0], C.int(len(a)))
// cgo 方案:C 侧编写 Wrapper 中转
int compare(const void *a, const void *b) {
    return goCompare((void *)a, (void *)b);
}
void sortInts(int *a, int n) {
    qsort(a, n, sizeof(int), compare);
}
//export goCompare
func goCompare(a, b unsafe.Pointer) C.int { ... }
a := []C.int{3, 1, 2}
C.sortInts(&a[0], C.int(len(a)))

相比之下,同样是调用 C 的 qsort,Zig 语言的实现则干净得多:

const c = @cImport({
    @cInclude("stdlib.h");
});

fn compare(...) callconv(.c) c_int { ... }

c.qsort(..., &compare);

这引出了我们的核心问题:能不能完整保留 Go 语言的开发体验,同时拥有 C 级别的原生互操作能力与 LLVM 后端能力?

基于 LLVM 的 Go 编译器 LLGo

为此,我们推出了 LLGo。用一句话概括:LLGo 让你用 Go 的语法写程序,用 LLVM 做编译,用 C 的方式和外面的世界互通。

核心架构设计

LLGo 的整体架构分为三个核心层次:

  • 前端(Go 类型检查与 SSA):基于 go/types 和 go/ssa 构建,最大化保持与官方 Go 语言语义的一致性。
  • 中后端(切换至 LLVM):将 Go SSA 转换为 LLVM IR,直接复用 LLVM 的代码优化能力与多目标平台生成支持,并在这一层完成 C ABI 兼容转换。
  • 互操作层(原生对接 C/C++/Python):告别 cgo,支持在 Go 代码中直接调用 C 库,并能导入 Python 模块。

回到前面 qsort 的例子,在 LLGo 中不再需要任何 C Wrapper 桥接,可以直接向 c.Qsort 传入一个 Go 匿名闭包函数完成排序:

import (
    "unsafe""github.com/goplus/lib/c"
)

values := []c.Int{3, 1, 2}
c.Qsort(c.Pointer(&values[0]), ...,
    func(a, b c.Pointer) c.Int { ... })
// values: [1 2 3]

三大 Go 编译器定位对比与构建体积实测

在 Go 生态中,官方编译器 gc、同样基于 LLVM 的 TinyGo 与 LLGo 面向不同的工程场景,各有所长:

表1

同时,LLGo 依然是一个兼容标准 Go 源码的编译器。以最基础的 hello.go 为例,同一份源码使用 go build 和 llgo build 编译,运行结果完全一致。但在 macOS 实测环境下,两者的构建产物大小有着显著差异:

  • go build 构建产物:2,429,874 B(约 2.43 MB)
  • llgo build 构建产物:1,280,832 B(约 1.28 MB)

实测显示,LLGo 生成的可执行文件体积约为官方 gc 的 53%,缩小近一半。此外,LLGo 原生支持将“Go + C”混合代码直接编译到 WebAssembly 目标环境运行。

llcppg 与 LLAR 生态

2

在编译器层面打通 ABI 只是第一步,真实的 C/C++ 跨平台工程构建往往更加棘手。

以太坊客户端 Go-Ethereum 用于访问 USB 硬件钱包的独立包 ethereum/hid 为例:为了兼容不同操作系统,该项目必须在 hidapi/ 目录下分别维护 Linux、macOS 和 Windows 等多套底层 C 源码。在进行交叉编译时,不仅必须开启 cgo 并配置目标平台的 C 交叉编译器,一旦关闭 cgo,设备枚举就会变为空。在更大型的 C/C++ 项目中,这类平台相关的源码与构建配置动辄数十个,维护成本很高。

为解决 C/C++ 库的符号引入与交叉编译难题,我们设计了一套由四个组件协同工作的工具链闭环(部分云端自动构建能力目前处于规划与推进阶段):

  • llcppg:读取 C/C++ 头文件并提取 AST,自动生成自包含的 Go 绑定包。
  • LLAR:类似 C++ 生态中的 Conan,使用 formula 描述如何获取、编译和安装一个 C/C++ 项目,并将复杂的交叉编译与工具链放到云端处理。
  • LLAR Hub 索引仓库:作为中心化的产物索引仓库,登记每个 C/C++ 项目的短名,并记录对应的 formula 与 llcppg 配置。社区只需向 llarhub/.index 提交 PR,合并后即可由 cibot 自动创建仓库并发布对应的 Go 包。
  • LLGo:直接调用 C/C++ 函数,并在链接阶段自动触发原生库的拉取与安装。

在这套体系成熟后,未来在 Go 中引入 C/C++ 依赖将变得和原生 Go 模块一样简单——无需手动折腾本地 C 工具链,执行两行命令即可开箱即用:

$ go get llar.io/zlib
$ llgo build

llcppg 如何生成 C/C++ 的 Go 绑定

在使用 llcppg 生成绑定包时,只需提供一份简单的 JSON 配置文件。llcppg 不仅能通过 FuncPrefix、TypePrefix 自动剥离 C 语言命名前缀以生成符合 Go 规范的命名,还能通过 MethodPrefix 将特定前缀的 C 函数自动转换为 Go 结构体的方法。

更具技术挑战性的部分是如何用 Go 表示那些“C/C++ 有、但 Go 语言没有”的底层语法特性? 针对联合体、位域和 C++ 虚函数,llcppg 均已落定对应的设计提案并完成了工程实现:

表2

以 C 语言的位域为例,一段包含 1 bit 字段的 C 结构体,经由 llcppg 处理后会自动生成如下类型安全的 Go 访问器代码:

// C 位域定义
struct Flags {
    unsigned int a : 1;
    ...
};
// llcppg 自动生成的 Go 访问器
func (p *Flags) XGof_get_a c.Uint {
    return c.Uint(bitfield.Unsigned(
        unsafe.Pointer(p), 0, 1))
}

目前,C/C++ 标准库已完整覆盖,同时我们已验证并上线了大批高频 C/C++ 开源库的 Go 绑定:

  • 数据与文本:cjson、libxml2、libxslt、sqlite、zlib
  • 安全:openssl
  • 基础设施与编译器:bdw-gc、libuv、llvm-c、clang-c
  • 脚本与图形:lua、raylib

连接 Python 与 AI 生态

除了打通 C/C++,LLGo 生态的另一块重要拼图是直接复用 Python 的 AI 与数据科学生态。 我们的技术路径分为两步:

  • 首先,通过 llcppg 将底层的 Python C API 转换为 Go 绑定包;
  • 随后,利用专门开发的 llpyg 工具,将 Python 的标准库与第三方库自动转换为对应的 Go 包。

目前,包括 py/math、py/json、py/inspect、py/statistics 在内的 Python 标准库,以及 py/numpy、py/pandas、py/torch、py/matplotlib 等主流第三方科学计算与深度学习库,均已完成绑定转换并支持在 LLGo 中直接调用:

import (
    "github.com/goplus/lib/py"
    "github.com/goplus/lib/py/math"
    "github.com/goplus/lib/py/std"
)

x := math.Sqrt(py.Float(2))
std.Print(py.Str("sqrt(2) ="), x)
// 输出:sqrt(2) = 1.414…

pydump 如何在同一个 Go 程序里调用 C 和 Python

为了直观展示 LLGo 跨生态混调的能力,我们可以看一个真实的内部工具——pydump。

pydump 是 llpyg 生成 Python 库绑定时的底层依赖工具,它的功能是输入一个 Python 库名,导出该库中所有符号的类型、名称、文档及函数签名,并格式化为 JSON 输出。

在使用 LLGo 编写 pydump 时,我们直接将 C 标准库、C 开源库、Python C API 和 Python 标准库揉合在了几十行 Go 代码中:

// 初始化 Python,创建 cJSON 根对象,导入目标 Python 模块
pyLib := c.Index(c.Argv, 1)
py.Initialize
root := cjson.CreateObject
root.AddItemToObject(c.Str("name"), cjson.CreateString(pyLib))
items := cjson.CreateArray
mod := py.ImportModule(pyLib)
keys := mod.ModuleGetDict.DictKeys

// 遍历模块符号,通过 Python inspect 提取签名,并用 cJSON 输出
for i, n := 0, keys.ListLen; i < n; i++ {
    key := keys.ListItem(i)
    val := mod.GetAttr(key)
    sym := cjson.CreateObject
    sym.AddItemToObject(c.Str("name"), cjson.CreateString(key.CStr))
    if val.Callable != 0 {
        sig := inspect.Signature(val)
        sym.AddItemToObject(c.Str("sig"), cjson.CreateString(sig.Str.CStr))
    }
    cjson.AddItemToArray(items, sym)
}
root.AddItemToObject(c.Str("items"), items)
c.Printf(c.Str("%s\n"), root.Print)

这段代码虽然完全使用 Go 语法编写,但没有引用任何 Go 标准库,全部能力均由底层的 C 与 Python 生态提供。在 macOS 下使用 llgo build 编译后,生成的 pydump 可执行文件体积仅有 53,152 B(约 52 KB)。

现场提问

提问:在端侧或嵌入式硬件等内存资源极其有限的场景下,考虑到常规 Go 运行时的垃圾回收(GC)是自动运行的,使用 LLGo 进行跨语言开发或运行在受限设备上时,内存是如何管理的?需要像 C/C++ 一样手动管理内存吗?

郭浩蓝:关于 GC,在 LLGo 中需要区分两个场景来看:一个是普通的 PC/服务端跨语言调用场景,另一个是嵌入式场景。

在普通 PC 调用场景下,因为 C 语言本身不支持也不受 GC 管理,所以我们遵循清晰的边界分离原则——C 生态一侧分配的内存依然由 C 自己管理。而在资源受限的嵌入式场景下,由于内存空间非常小,我们目前采取的方案是将整个嵌入式运行环境直接统一接入到我们的保守式 GC 算法中进行托管。

posted @ 2026-11-10 15:42  小七-七牛开发者  阅读(5)  评论(0)    收藏  举报