23 人が閲覧(直近 30 日) Go

TinyGoとは?通常のGoとの違い・小さくなる仕組み・Lチカ・Wasmを0.43.0で実測

TinyGoとは?通常のGoとの違い・小さくなる仕組み・Lチカ・Wasmを0.43.0で実測

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つの場面です。

日本語で学べる場: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のフルパスを指定します。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

RELATED POSTS 関連記事

目次