TinyGo 深度拆解:用 Go 写嵌入式和 WebAssembly,LLVM 后端是怎么做到的
来源:微信公众号
Go 的标准编译器(gc)是为服务器和云计算设计的,生成的二进制最小也有几 MB,跑在内存几百 KB 的微控制器上不现实。
TinyGo 解决的就是这个问题——它用 LLVM 做后端,能把 Go 代码编译到 AVR(Arduino)、ARM Cortex-M(BBC micro:bit、Adafruit)、RISC-V,还有 WebAssembly。编译出来的二进制可以小到几 KB。GitHub ⭐ 15K+,作者 TinyGo 社区。
为什么写 TinyGo
Go 的嵌入式能力一直被忽视。大家觉得 Go 就是写后端服务的,嵌入式是 C 和 Rust 的地盘。但 TinyGo 改变了这件事:有人用 Go 写了一个温湿度传感器的驱动,编译出来只有 8KB,直接刷到 Arduino Nano 上跑起来了。
TinyGo 不是简单地把 Go 裁小——它重新设计了一套编译流程:
- 用 LLVM 做优化
- 实现了 Go 的子集
- 支持 goroutine 和 channel(在嵌入式上)
- 还能编译成 WebAssembly 跑在浏览器里
项目信息
| 项目 | 信息 |
|---|---|
| GitHub | github.com/tinygo-org/tinygo |
| Star | 15K+ |
| 语言 | Go + LLVM(C++) |
| 协议 | BSD-3-Clause |
| 维护方 | TinyGo 社区(Hybrid Group 赞助) |
| 编译器后端 | LLVM |
| 支持平台 | Arduino、BBC micro:bit、Adafruit、ESP32、ESP8266、STM32、RISC-V、WebAssembly |
| Go 版本兼容 | Go 1.22+ |
| 最小二进制 | 几 KB(裸机 AVR) |
Go 编译器 vs TinyGo:两套思路
Go 官方的编译器(gc)靠的是"快速编译 + 够用的性能",不做复杂优化,生成的二进制通常比较大(几 MB 起步)。
TinyGo 的思路完全不同:用 LLVM 做后端。
LLVM 是编译器的"瑞士军刀"——Clang、Rust 编译器、Swift 编译器都用 LLVM 做后端。TinyGo 把 Go 代码先转成 LLVM IR(中间表示),然后让 LLVM 做优化和代码生成。
Go 源码 → TinyGo 前端(类型检查、AST→IR)→ LLVM IR → LLVM 优化 → 机器码/WebAssembly
这种方式的代价是编译速度比 gc 慢很多,但生成的二进制小得多。
嵌入式上的 Go:goroutine 在微控制器上
TinyGo 在微控制器上支持 goroutine 和 channel。
Arduino 的 ATmega328P 只有 2KB RAM、32KB Flash,在这种环境下跑 goroutine?听起来不可能。
TinyGo 的实现方式:每个 goroutine 的栈不是从堆上分配的,而是编译时静态分配的。TinyGo 在编译时分析每个 goroutine 的最大栈深度,预留固定大小的栈空间。
goroutine 的数量在编译时就确定了,不能像标准 Go 那样动态创建几万个。但对于嵌入式场景来说,十几个 goroutine 就够用了——一个读传感器、一个处理数据、一个控制 LED,很够了。
Channel 也是类似的处理:TinyGo 的 channel 是同步的(无缓冲或者固定缓冲),实现比标准 Go 简单得多。
// 这段代码可以编译到 Arduino 上
package main
import (
"machine"
"time"
)
func main() {
led := machine.LED
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
go blink(led, 200*time.Millisecond)
go blink(led, 500*time.Millisecond)
select {} // 阻塞,让 goroutine 跑
}
func blink(led machine.Pin, interval time.Duration) {
for {
led.High()
time.Sleep(interval)
led.Low()
time.Sleep(interval)
}
}
两个 goroutine 同时闪 LED,一个快一个慢。这在 Arduino 上真的能跑。
WebAssembly:Go 跑在浏览器里
TinyGo 的另一个重要场景是 WebAssembly(Wasm)。
Go 官方编译器也支持 Wasm 编译(GOOS=js GOARCH=wasm),但生成的二进制有 8MB+,浏览器加载要好几秒,基本没法用。
TinyGo 编译的 Wasm 二进制可以压到 几十 KB 到几百 KB。原因是 LLVM 做了激进的死代码消除——没用到的标准库函数不会被编译进去。
# 编译成 WebAssembly
tinygo build -o main.wasm -target=wasm ./cmd/main
| 维度 | Go 官方编译器 | TinyGo |
|---|---|---|
| Wasm 二进制大小 | ~8MB | ~几十 KB |
| 编译速度 | 快 | 慢 |
| Go 特性完整度 | 完整 | 子集 |
| 浏览器加载时间 | 几秒 | 毫秒级 |
硬件抽象层:machine 包
TinyGo 提供了一套硬件抽象层(machine 包),让你用统一的 API 操作不同硬件的 GPIO、SPI、I2C、UART、ADC 等:
import "machine"
// 不管底层是 Arduino 还是 ESP32,API 一样
pin := machine.D2
pin.Configure(machine.PinConfig{Mode: machine.PinOutput})
pin.High()
这套抽象层让 Go 代码可以在不同硬件之间移植。TinyGo 社区还维护了很多外设驱动(tinygo.org/x/drivers):温湿度传感器、OLED 屏幕、步进电机、GPS 模块……都是纯 Go 写的。
编译流程:LLVM 的魔力
- Go 源码解析:TinyGo 用 Go 的标准
go/ast包解析源码,做类型检查 - IR 生成:把 Go 的 AST 转成 LLVM IR。这步最复杂——需要处理 interface、goroutine、defer、recover 等特性
- LLVM 优化:死代码消除是这里做的——没用到的函数和全局变量会被删掉
- 代码生成:LLVM 生成目标平台的机器码(AVR、ARM、RISC-V)或者 WebAssembly
- 链接:跟 TinyGo 的 runtime(精简版 GC、调度器)链接,生成最终二进制
TinyGo 的 runtime 也是专门为嵌入式设计的:官方的 Go runtime 有几 MB,TinyGo 的 runtime 只有几 KB。GC 用的是标记-清除的简化版本,内存开销很小。
跟其他方案对比
| 维度 | TinyGo | Go 官方编译器 | Rust + Embassy | C/Arduino |
|---|---|---|---|---|
| 目标平台 | 微控制器 + Wasm + Linux | 服务器/桌面 | 微控制器 + 通用微控制器 | 嵌入式 |
| 二进制大小 | 几 KB ~ 几百 KB | 几 MB 起 | 几 KB ~ 几十 KB | 几 KB |
| 并发模型 | goroutine/channel | goroutine/channel | async/await | 无(中断驱动) |
| 编译速度 | 慢(LLVM) | 快 | 慢(LLVM) | 快 |
| 内存安全 | 有(GC) | 有(GC) | 有(所有权) | 无 |
| 生态 | 硬件驱动库 | 服务器生态 | Embassy 生态 | 巨大 |
TinyGo 是唯一一个让你在微控制器上用 goroutine 的编译器。
不足
- 编译速度慢:LLVM 的优化是慢的,开发体验不太好
- Go 特性不完整:reflection 支持有限,sync 包不完整,很多标准库依赖不可用
- 调试困难:GDB 支持有限,fmt.Println 在微控制器上不一定能用
- LLVM 依赖:需要安装 LLVM,不同平台版本不一,安装过程不如 Go 官方编译器丝滑
- 社区规模:15K Star,但活跃贡献者不多
适用场景
- 嵌入式开发:在 Arduino、ESP32、STM32 上写程序,又不想学 C 或 Rust
- WebAssembly:需要在浏览器或边缘计算平台跑 Go 代码,官方编译器的 Wasm 太大
- IoT 原型开发:快速搭一个 IoT 原型
- 教育:用 TinyGo 教嵌入式编程,Go 的语法比 C 简单得多
总结
TinyGo 把 Go 带到了微控制器和浏览器里。它不是 Go 的替代品——不会用 TinyGo 来写后端服务。它是一个补充:在 Go 官方编译器覆盖不到的地方,TinyGo 补上了。
LLVM 后端是 TinyGo 的核心选择。代价是编译速度慢,收益是二进制小、优化好。对于嵌入式和 Wasm 场景,这个 trade-off 是值得的。
GitHub:https://github.com/tinygo-org/tinygo