TinyGoは、Go言語のソースコードをマイコンやWebAssembly(Wasm)向けの小さなバイナリにコンパイルする、LLVMベースの別実装コンパイラです。同じ「Hello World」を通常のGoでビルドすると約2.5MB、TinyGo 0.43.0では約240KBでした。2026年10月9日公開の0.43.0で、サイズ差と小さくなる仕組み、インストール、PicoとESP32のLチカ、Wasmのターゲットを実測しました。
まとめ:TinyGoの要点と向いている場面
- TinyGoはGo公式コンパイラ(gc)とは別のコンパイラで、コード生成にLLVM 22を使います。現行版は0.43.0(2026年10月9日)で、組み合わせるGoツールチェーンは1.25〜1.27です。
- fmtでHello Worldを出すプログラムは、通常のGoで2,504,528バイト、TinyGoで239,936バイトでした(約10分の1)。WASI向けは2,479,945バイトに対して179,256バイトです。
- 小さくなる理由は、全体を1回でコンパイルして不要なコードを落とすこと、初期化をコンパイル時に済ませること、GCとスケジューラを選べることです。Wasmでは
-scheduler=noneだけで179,256バイトが27,030バイトに減りました。 - 0.43.0からWasmでもpanicを
recover()で回復できます。公式の言語サポートページはまだ「WebAssemblyでは不可」のままです。 - Raspberry Pi Pico向けのLチカはflash 11,700バイトです。encoding/jsonを入れると、Go 1.27では347,820バイトに膨らみ、XIAO(SAMD21)のflashには収まりませんでした。Wasmのビルドにはbinaryenの
wasm-optが別途必要です。
TinyGoの定義と現行版0.43.0の位置づけ
開発元はGitHubのtinygo-org/tinygoで、リポジトリの説明は「Go compiler for small places」です。Go本体とは別のプロジェクトで、型チェックと標準ライブラリはインストール済みのGoのものを使い、コード生成と最適化をLLVMに任せます。そのためGo本体が先に必要です。Go本体の導入から始める場合はGo言語とは|1.27系の導入からHTTP APIを動かすまでの実装入門を先に済ませてください。
対応するGoの版はソースコードに定数で書かれています。0.43.0のbuilder/config.goはminorMin = 25、minorMax = 27で、範囲外のGoでは「requires go version 1.25 through 1.27」というエラーで止まります。公式サイトのインストール手順は「Go v1.23 or above」のままなので、Go 1.23や1.24の環境では手順どおりに入れても動きません。
| 版 | 公開日(UTC) | 主な変更 |
|---|---|---|
| 0.43.0 | 2026-10-09 | Wasm・Xtensaでのrecover、-trimpath、ESP32-H2 |
| 0.42.0 | 2026-09-01 | Go 1.27・LLVM 22対応、ランタイムpanicのrecover |
| 0.41.1 | 2026-04-22 | 0.41.0の修正版 |
| 0.41.0 | 2026-04-21 | 前回のマイナー版 |
0.43.0のリリースノートはGoとの互換性に重点を置いた版だと説明しており、synctest、reflectのNewAtとSlice3、ESP32とnRF52のI2Sなどが入りました。再現可能なビルド用の-trimpathも追加され、wasip1向けに2回ビルドした出力のSHA-1は一致しました。版番号はまだ0.x系で、互換性を約束する1.0には達していません。
通常のGo(gc)とのバイナリサイズの差
fmt.Println("hello, tinygo")だけのmain関数を、go buildとtinygo build(Wasmは-target=wasip1など)でビルドし、出力ファイルのバイト数を比べました。環境はmacOS 26.6(Intel)、Go 1.27.2、TinyGo 0.43.0、測定日は2026年10月23日です。TinyGoのWasm出力には-no-debugを付けています。
| ビルド対象 | 通常のGo(バイト) | TinyGo(バイト) |
|---|---|---|
| macOSネイティブ(fmt.Println) | 2,504,528 | 239,936 |
| 同上(Goは-s -w付き) | 1,628,544 | 239,936 |
| WASI(wasip1・fmt.Println) | 2,479,945 | 179,256 |
| ブラウザ(js/wasm・fmt.Println) | 2,506,776 | 159,433 |
| WASI(組み込みのprintln) | 1,910,065 | 24,262 |
fmtをやめて組み込みのprintlnにすると、TinyGoのwasip1出力は179,256バイトから24,262バイトに縮みます。通常のGoは1,910,065バイトで、大きくは減りません。-no-debugを外すとwasip1は650,268バイトになるので、配布するWasmには付けます。
サイズはTinyGoの版だけでなく、組み合わせるGoの版でも動きます。同じGo 1.27.2で0.42.0に戻すと、wasip1のHello Worldは98,016バイト、ネイティブは218,808バイトでした。0.43.0ではそれぞれ179,256バイトと239,936バイトに増えています。原因までは切り分けていませんが、0.43.0にはAsyncifyを使ってpanicを回復する実装が入っています。サイズを詰めているなら、版を上げる前後で出力を比べてください。
TinyGoが小さくなる仕組み:全体コンパイル・初期化・ランタイムの選択
TinyGoのバイナリが小さい理由は、公式ドキュメントのコンパイルの流れ(Pipeline)とGoとの違いに書かれています。大きく分けて次の3つです。
プログラム全体の一括コンパイルと不要コードの削除
TinyGoはプログラム全体を、途中のリンクを挟まず1回でコンパイルします。大きなプログラムではビルドが遅くなる代わりに、最適化の余地が広がると公式は説明しています。GoのSSAをLLVM IRに落とし、LLVMの最適化とTinyGo独自のエスケープ解析などをかけます。全体が見えているので、どこからも呼ばれない関数や使われないグローバル変数を丸ごと落とせます。
パッケージ初期化のコンパイル時評価
通常のGoのコンパイラも、定数だけで決まる初期値は静的データとして置きますが(staticinit)、関数呼び出しを含む初期化は実行時のinit処理に残ります。TinyGoはこうした初期化関数も、コンパイル時にできるところまで解釈して実行します。初期化の命令列の代わりに初期値のデータだけをflashに置けばよく、定数とわかったグローバル変数はRAMではなくflashに置かれます。
GC・スケジューラ・panic処理の設定別サイズ比較
3つめはランタイムの選択です。公式の最適化ガイドが挙げるオプションを、fmtのHello World(wasip1)とPicoのLチカで1つずつ試しました。既定の最適化は-opt=z(サイズ優先)です。
| オプション | wasip1(バイト) | Pico flash(バイト) |
|---|---|---|
| 既定 | 179,256 | 11,700 |
| -opt=s | 184,450 | 12,304 |
| -opt=2 | 258,207 | 24,608 |
| -gc=leaking | 151,278 | 10,596 |
| -panic=trap | 115,834 | 10,116 |
| -scheduler=none | 27,030 | 10,424 |
| 上の3つを併用 | 11,511 | 8,320 |
Wasmで効くのは-scheduler=noneです。wasip1の既定のスケジューラはasyncifyで、goroutineの切り替えのために関数を書き換えるAsyncifyがバイナリ全体を膨らませます。これを外すと85%減りました。一方Picoでは、ランタイムを削る3つのオプションの効果は1.1〜1.6KBずつ、併用でも3,380バイトです。速度優先の-opt=2は逆に2倍以上の24,608バイトになりました。-gc=noneはPicoのLチカでも「object allocated on the heap with -gc=none」でビルドに失敗し、ランタイムとerrorsパッケージのヒープ確保が原因として表示されました。
どのオプションにも代償があります。-scheduler=noneはgo文が使えなくなり、Wasmではpanicの回復もできなくなります(次章)。-panic=trapはpanicのメッセージを出さずに止まり、-gc=leakingは確保したメモリを一切解放しません。3つの併用(Hello Worldで11,511バイト)が向くのは、呼び出しごとにインスタンスを作り直すか、ヒープ確保をしない計算関数をWasmで公開する場合です。同じインスタンスで確保を繰り返すとメモリが枯渇するので、常駐するマイコンのプログラムでは既定のまま使います。
ターゲット別のGCとスケジューラの既定値
既定のGCとスケジューラはターゲットで違い、tinygo info -target=<ターゲット名>で確認できます。0.43.0の値は次のとおりです。
| ターゲット | GC | スケジューラ |
|---|---|---|
| pico・esp32c3-generic | conservative | tasks |
| arduino(AVR) | conservative | none |
| wasip1 | precise | asyncify |
| wasm-unknown | leaking | none |
通常のGoのgoroutineはM:Nスケジューリングで複数スレッドに載りますが(仕組みはゴルーチン(Goroutine)とは?Go言語の並行処理を支える軽量スレッドの仕組みと使い方)、TinyGoのtasksは単一コア上の協調的な切り替えです。メインループでのヒープ確保を避けたい場合は、-print-allocs=.を付けてビルドすると、ヒープに逃げた箇所がファイル名と行番号で表示されます。
recover・reflect・encoding/jsonの対応状況
公式の言語サポートページは冒頭に「as of june 2023」とあり、recoverは「WebAssemblyを除く大半のアーキテクチャで対応」「ランタイムpanicは回復できない」と書かれています。0.43.0ではどちらも当てはまりません。ゼロ除算とnil mapへの書き込みをrecover()で受けるコードを試すと、macOSネイティブでも、wasip1でビルドしてNode.js 26.5.0のWASIで動かした場合でも、両方とも回復しました。0.42.0ではwasip1の同じコードがpanicで止まっていました。ただし-scheduler=noneを付けたwasip1では回復できず、「panic: runtime error: integer divide by zero」で止まりました。Wasmでrecoverを使うなら、前章の-scheduler=noneによる縮小とは両立しません。
reflectは再実装されていて大半は動きますが、一部は未対応です。cgoの#cgo行も部分対応にとどまります。既存のGoライブラリを使うなら、対象ターゲットで実際にビルドして動くかを確かめてから採用します。
encoding/jsonは0.43.0でMarshalとUnmarshalとも正しく動きましたが、サイズは大きくなります。構造体1つを変換するだけのプログラムをPico向けにビルドするとflashは347,820バイト、静的RAMは30,572バイトで、Lチカの約30倍です。flash 256KBのXIAO(SAMD21)向けは「flash overflowed by 91588 bytes」でビルドできませんでした。10月3日に0.42.0とGo 1.26.5で測ったときは148,432バイトで、0.42.0のままGoを1.27.2にすると342,620バイトでした。増えた分はGo 1.27の標準ライブラリ側から来ているので、flashの小さいボードではGoの版を上げる前にサイズを確かめてください。
インストール手順:macOS・Windows・Linux
どのOSでも先にGo 1.25〜1.27を入れます。Wasmを出力するにはbinaryenのwasm-optが必要で、0.43.0のmacOS用tarballを展開しただけの状態では、wasip1のビルドが「error: could not find wasm-opt, set the WASMOPT environment variable to override」で止まりました。binaryen(2026年10月時点の最新はversion_133)を入れてPATHを通すと解消します。
macOSでの導入
Homebrewの専用tapから入れます。tapは2026年10月23日時点で0.43.0を指しています。Homebrew 6.0.0からは公式以外のtapに明示的な信頼が必要になり、brew tapのあとに短い名前でbrew install tinygoとすると止まります。完全修飾名で指定すれば、そのformulaだけを信頼して入れられます。
brew install tinygo-org/tools/tinygo
brew install binaryen
tinygo version
Intel Macでは「tinygo version 0.43.0 darwin/amd64 (using go version go1.27.2 and LLVM version 22.1.4)」と表示されました。tarballで入れる場合はtinygo0.43.0.darwin-amd64.tar.gz(Intel)かdarwin-arm64(Apple Silicon)を展開し、tinygo/binをPATHに追加します。
Windowsでの導入
公式はScoopでの導入を案内しています。Scoopを使わない場合はtinygo0.43.0.windows-amd64.zipを展開し、binフォルダをユーザー環境変数のPATHに追加します。
scoop install go
scoop install tinygo
scoop install binaryen
Linux(Ubuntu・Debian)での導入
amd64・arm64・armhf向けの.debがGitHub Releasesで配布されています。
wget https://github.com/tinygo-org/tinygo/releases/download/v0.43.0/tinygo_0.43.0_amd64.deb
sudo dpkg -i tinygo_0.43.0_amd64.deb
sudo apt install binaryen
実機への書き込みに使うツールはボードのターゲット定義で決まります。Arduino UnoなどAVRマイコンはavrdude、Arduino Nano 33 IoTはbossacが必要です。PicoやXIAOのように書き込み方式がmsd(USBマスストレージ)のボードは追加のツールが要りません。
ハードウェアなしで試す:TinyGo Playground
ボードが手元に無い段階では、公式のTinyGo Playgroundで試せます。コードはサーバーで実行するのではなくWasmにコンパイルしてブラウザで動かし、いくつかのボードをブラウザ上でシミュレーションします。ソースコードのボード定義には、Raspberry Pi Pico、Arduino Uno、Arduino Nano 33 IoT、BBC micro:bit v1、Seeed Studio XIAO ESP32-S3、Gopher Badgeなどが並んでいます。公式は「シミュレーションは実機の挙動と異なる場合がある」と注意しており、ドラッグ&ドロップで書き込めるボードならFlashボタンからそのまま実機へ書き込めます。
マイコンでLチカ:Raspberry Pi PicoとESP32
TinyGo 0.43.0のtinygo targetsは199件のターゲット名を返しました。ボード上のLEDを0.5秒ごとに点滅させるコードは次のとおりです。
package main
import (
"machine"
"time"
)
func main() {
led := machine.LED
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
led.High()
time.Sleep(500 * time.Millisecond)
led.Low()
time.Sleep(500 * time.Millisecond)
}
}
go mod init blinkでモジュールを作り、ボードを接続して書き込みます。-size shortを付けると、ビルド時に集計したflash(code+rodata+data)と静的RAM(data+bss)が表示されます。実行時のヒープとスタックは含みません。
tinygo flash -target=pico .
tinygo build -target=pico -size short -o blink.uf2 .
Picoの書き込み方式はmsdで、RPI-RP2という名前のドライブにUF2ファイルをコピーします。初回はBOOTSELボタンを押しながらUSBを挿します。2回目以降はTinyGoがボードをブートローダーへ再起動させます。Pico Wとの違いはRaspberry Pi Pico Wとは?WH・Pico 2 Wとの違い、できることとWi-Fi接続【2026年10月】で整理しています。
主要ボード向けのビルド結果です(単位はバイト・実機での点滅は未確認)。
| ターゲット | flash | 静的RAM |
|---|---|---|
| pico(RP2040) | 11,700 | 6,188 |
| pico2(RP2350) | 10,340 | 6,180 |
| xiao(SAMD21) | 8,220 | 5,388 |
| arduino-nano33 | 8,236 | 5,388 |
| arduino(Uno・AVR) | 928 | 560 |
machine.LEDが無いボードの書き方
microbitとESP32の汎用ターゲットでは、上のコードが「undefined: machine.LED」で失敗します。ボード定義にオンボードLEDの定数が無いためで、ピン番号を直接指定します。次はESP32-C3のGPIO8に抵抗経由で外付けLEDをつないだ想定で、main関数の中身を置き換える例です。
led := machine.GPIO8
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
led.Set(!led.Get())
time.Sleep(500 * time.Millisecond)
}
ビルド結果のflashはesp32c3-genericが6,666バイト、esp32-genericが8,664バイト、esp32s3-genericが7,842バイトでした(実機での点滅は未確認)。ESP32-WROOM系ではGPIO6〜11がSPI flashの接続に使われているため、ピン番号を機種間で流用せず、ボードの回路図で空いているGPIOを選んでください。ESP32系の書き込み方式はesp32flashで、USBシリアル経由です。ESP32を積んだ完成品の仕様はM5Stack Core2とは?バッテリー容量・電源管理とv1.1/v1.3の違い、できること【2026年10月】が参考になります。
センサーを読む:tinygo.org/x/driversのBME280
センサーや表示器のドライバは、別リポジトリのtinygo-org/driversにまとまっています。READMEには145種類以上のドライバを収録とあり、最新はv0.36.0(2026年9月1日)です。温湿度・気圧センサーBME280をI2Cで読む例は次のとおりで、go get tinygo.org/x/[email protected]で取り込みます。
package main
import (
"machine"
"time"
"tinygo.org/x/drivers/bme280"
)
func main() {
machine.I2C0.Configure(machine.I2CConfig{})
sensor := bme280.New(machine.I2C0)
sensor.Configure()
if !sensor.Connected() {
println("BME280が見つかりません。配線とI2Cアドレスを確認してください")
return
}
for {
t, err := sensor.ReadTemperature()
if err != nil {
println("温度の読み取りに失敗:", err.Error())
time.Sleep(2 * time.Second)
continue
}
h, err := sensor.ReadHumidity()
if err != nil {
println("湿度の読み取りに失敗:", err.Error())
time.Sleep(2 * time.Second)
continue
}
println("temp(m°C):", t, "hum(0.01%):", h)
time.Sleep(2 * time.Second)
}
}
ピンを指定しないI2CConfig{}では、PicoのI2C0はSDAがGP4、SCLがGP5になります。ReadTemperatureは1000分の1度、ReadHumidityは0.01%単位の整数を返すので、表示するときに割り算します。センサーが見つからないときは読み取りに進まず終了し、読み取りに失敗したときは値を表示せず2秒後に再試行します。このコードのPico向けビルドはflash 18,088バイト・静的RAM 6,248バイトでした(実機での読み取りは未確認)。
WebAssemblyで使う:4つのターゲットの違い
Wasm向けのターゲットは実行先で選び分けます。WebAssembly自体の仕組みはWebAssembly(Wasm)とは?JavaScript連携とMemory・Table操作の実装を参照してください。
| -target | 実行先 | 追加で必要なもの |
|---|---|---|
| wasm | ブラウザ・Node.js | TinyGo同梱のwasm_exec.js |
| wasip1 | WASIランタイム | なし |
| wasip2 | コンポーネントモデル対応ランタイム | wasm-tools |
| wasm-unknown | ホスト関数を持たない環境 | なし |
-target=wasmの出力は、通常のGoが配るwasm_exec.jsではなく、TinyGo側の$(tinygo env TINYGOROOT)/targets/wasm_exec.jsと組み合わせます。ファイルはTinyGoと同じ版のものを使います(公式のWasmガイド)。wasip2はビルドの最後にwasm-tools component embedを呼ぶため、wasm-toolsが無い環境では「`wasm-tools component embed` failed」で失敗しました。WASIの仕様と各版の違いはWASIとは|WebAssemblyをブラウザ外で動かすシステムインターフェース【2026年版】で解説しています。
関数を1つだけ公開する748バイトのWasm
計算処理だけをJavaScriptから呼びたい場合は、wasm-unknownと//go:wasmexportを組み合わせます。wasm-unknownはWASIやJavaScriptのホスト関数を前提にせず、GCは解放しないleaking、スケジューラはnoneなので、ランタイムがほとんど入りません。
package main
//go:wasmexport add
func add(a, b int32) int32 {
return a + b
}
func main() {}
tinygo build -target=wasm-unknown -no-debug -o add.wasm .
出力は748バイトでした。次の内容をrun.cjsとして保存し、node run.cjsで実行すると5が表示されます。
const fs = require('node:fs');
(async () => {
const { instance } = await WebAssembly.instantiate(fs.readFileSync('add.wasm'), {});
instance.exports._initialize();
console.log(instance.exports.add(2, 3));
})();
_initialize()を飛ばすと「RuntimeError: unreachable」で落ちます。公式ガイドの例は//exportという旧来の書き方ですが、Go本体と同じ//go:wasmexportが使えます。Rustで同じことをする場合の手順とサイズの比較軸はRustでWebAssemblyを実装する手順|wasm-packでのWASM化と軽量化・配置先の選び方【2026年版】にまとめています。
TinyGoを選ぶ場面と避ける場面
TinyGoが最も効くのは、Goを書けるチームがRP2040・SAMD21・nRF52のようなCortex-Mマイコンで、センサー読み取りや表示、USB HIDといった制御を書く場面です。Lチカはflash 11.7KBに収まり、goroutine・channel・selectもそのまま使えます。マイコンとクラウドをつなぐ仕組みまで含めて検討するなら、弊社のAI/IoTソリューションでも構成の相談を受けています。Wasmでは、通常のGoで2.5MBになるモジュールを数十KB〜180KBに縮めたい場合に選びます。
避けるべきなのは次の3つの場面です。
- reflectに依存するライブラリを多数取り込むサーバー処理をWasmへ移植する場合。ビルドが通っても挙動が変わることがあり、encoding/jsonだけでもマイコンでは300KBを超えます。通常のGoのwasip1出力(2.5MB)で困らないなら、gcのまま使う方が安全です。
- 厳密な応答時間の保証が要る制御。TinyGoのスケジューラは協調的な切り替えで、GCも停止を伴います。割り込み応答の上限を保証したいなら、RTOSとは?リアルタイムOSの仕組み・主要製品の比較と採用判断を実装者目線で解説で扱うRTOSとC/C++の組み合わせを選びます。OSを載せるかどうかの線引きは組み込みOSとは?3分類の境界とITRON系の位置づけ・選定判断を実装者向けに解説が参考になります。
- ArduinoのC++ライブラリをそのまま使いたい場合。TinyGoに移るよりarduino-cliとは?インストールからコンパイル・書き込み・CI自動化まで実践ガイドでビルドを自動化する方が早く済みます。
日本語で学べる場:TinyGo Keebと書籍
国内では、自作キーボードとTinyGoを組み合わせたコミュニティTinyGo Keebが活動しています。はんだ付けからTinyGoでのファームウェア作りまでを1日で行うハンズオン「TinyGo Keeb Tour」を2024年から全国で開いており、connpassのグループによると、2026年10月10日に秋葉原でTinyGo Conference 2026 in JAPANを開催し、次回のTourは11月7日の福井県鯖江市です。書籍では高砂正哲『基礎から学ぶTinyGoの組込み開発』(シーアンドアール研究所、2022年11月)があります。刊行から約4年経つため、版に依存するコマンドはリリースノートと照らして読みます。
よくある質問
TinyGoとは何ですか?
GoのソースコードをマイコンやWebAssembly向けに小さくコンパイルする、LLVMベースの別実装コンパイラです。現行版は0.43.0(2026年10月9日公開)で、Go 1.25〜1.27のツールチェーンと組み合わせて使います。
TinyGoのGitHubリポジトリはどこですか?
github.com/tinygo-org/tinygo です。リリースファイル(.deb、.zip、macOS用tarball)は同リポジトリのReleasesから入手でき、センサー等のドライバは別リポジトリのtinygo-org/drivers、公式ドキュメントはtinygo.orgにあります。
TinyGoと通常のGoは何が違いますか?
プログラム全体をLLVMで一括コンパイルし、初期化をコンパイル時に済ませ、ランタイムを小さくしている点です。fmtのHello Worldは通常のGoで2,504,528バイト、TinyGoで239,936バイトでした。代わりにreflectやcgoは一部しか実装されていません。
TinyGoはESP32で使えますか?
使えます。0.43.0にはesp32-generic、esp32c3-generic、esp32s3-genericなどのターゲットがあり、GPIOを指定したLチカは3種類ともビルドできました。汎用ターゲットにはmachine.LEDが無いため、ピン番号を直接指定します。0.43.0ではESP32のPWMとI2Sにも対応しました。
TinyGoでWasmをビルドすると「could not find wasm-opt」と出るのはなぜですか?
binaryenのwasm-optが見つからないためです。macOSはbrew install binaryen、Windowsはscoop install binaryen、Ubuntuはsudo apt install binaryenで入れます。導入済みなのに出る場合はPATHを確認するか、環境変数WASMOPTにwasm-optのフルパスを指定します。