
こんにちは。よっしーです(^^)
本日は、Go言語のよくある質問 について解説しています。
背景
Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。
Implementation
なぜ些細なプログラムのバイナリがこんなに大きいのか?
gc
ツールチェーンのリンカーは、デフォルトで静的リンクされたバイナリを作成します。したがって、すべてのGoバイナリにはGoランタイムが含まれ、動的型チェック、リフレクション、さらにはパニック時のスタックトレースをサポートするために必要なランタイム型情報も含まれています。
Linuxでgccを使用して静的にコンパイルおよびリンクされた単純なC言語の”hello, world”プログラムは、printf
の実装を含めて約750 KBです。fmt.Printf
を使用する同等のGoプログラムは数メガバイトの重さがありますが、これにはより強力なランタイムサポートと型情報およびデバッグ情報が含まれています。
gc
でコンパイルされたGoプログラムは、-ldflags=-w
フラグを使用してリンクすることでDWARF生成を無効化でき、バイナリからデバッグ情報を削除できますが、他の機能の損失はありません。これによりバイナリサイズを大幅に削減できます。
解説
この問題は何について説明しているの?
Goで簡単なプログラムを書いてコンパイルすると、実行ファイルのサイズが意外と大きいことに驚くかもしれません。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
このたった数行のプログラムが、数MBの実行ファイルになるのはなぜ? という疑問に答えます。
基本的な用語
- バイナリ: コンパイル後の実行可能ファイル
- 静的リンク: 必要なライブラリをすべて実行ファイルに含める方式
- 動的リンク: 実行時に外部ライブラリを読み込む方式
- ランタイム: プログラム実行時に必要な支援システム
- リフレクション: プログラムが自分自身の構造を調べる機能
- DWARF: デバッグ情報の形式
実際のサイズを見てみよう
Hello Worldプログラム
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
コンパイル:
go build -o hello hello.go
ls -lh hello
結果:
-rwxr-xr-x 1 user staff 1.8M hello
約1.8MB! たった3行のプログラムなのに!
C言語との比較
C言語版:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
コンパイル(動的リンク):
gcc -o hello hello.c
ls -lh hello
結果:
-rwxr-xr-x 1 user staff 16K hello
約16KB! Goの100分の1以下!
コンパイル(静的リンク):
gcc -static -o hello hello.c
ls -lh hello
結果:
-rwxr-xr-x 1 user staff 750K hello
約750KB。静的リンクでも、Goより小さい。
なぜGoのバイナリは大きいのか?
理由1: Goランタイムが含まれる
Goバイナリの中身:
┌─────────────────────────┐
│ あなたのコード (数KB) │
├─────────────────────────┤
│ Goランタイム (1-2MB) │
│ - ゴルーチンスケジューラ │
│ - ガベージコレクタ │
│ - スタック管理 │
│ - チャネル実装 │
└─────────────────────────┘
C言語の場合:
┌─────────────────────────┐
│ あなたのコード │
│ (数KB) │
└─────────────────────────┘
↓
システムのライブラリに依存(外部)
理由2: 型情報が含まれる
Goはリフレクションや動的型チェックをサポートするため、型情報を含みます:
package main
import (
"fmt"
"reflect"
)
func main() {
x := 42
// 実行時に型を調べられる
t := reflect.TypeOf(x)
fmt.Println(t) // "int"
}
この機能のため、すべての型の情報がバイナリに含まれます。
理由3: デバッグ情報
パニック時に詳細なスタックトレースを表示するため:
package main
func a() { b() }
func b() { c() }
func c() { panic("error!") }
func main() {
a()
}
実行結果:
panic: error!
goroutine 1 [running]:
main.c(...)
/path/to/main.go:5
main.b(...)
/path/to/main.go:4
main.a(...)
/path/to/main.go:3
main.main()
/path/to/main.go:8
この詳細な情報を表示するため、デバッグ情報が含まれています。
静的リンク vs 動的リンク
静的リンク(Goのデフォルト)
実行ファイル
┌──────────────────────┐
│ プログラム │
│ + 必要なライブラリ全て│
└──────────────────────┘
↓
どこでも動く! (依存なし)
利点:
- ✅ 単一ファイルで完結
- ✅ 他のライブラリ不要
- ✅ デプロイが簡単
- ✅ バージョン問題なし
欠点:
- ❌ ファイルサイズが大きい
動的リンク(C言語の一般的な方法)
実行ファイル(小さい)
┌──────────────┐
│ プログラム │
└──────────────┘
↓ 依存
システムのライブラリ
┌──────────────┐
│ libc.so │
│ (共有) │
└──────────────┘
利点:
- ✅ ファイルサイズが小さい
- ✅ ライブラリを複数プログラムで共有
欠点:
- ❌ 依存ライブラリが必要
- ❌ バージョン問題が起こりうる
- ❌ デプロイが複雑
Goが静的リンクを選んだ理由
1. デプロイの簡単さ
# Goの場合
$ scp hello server:/usr/local/bin/
$ ssh server /usr/local/bin/hello
# → すぐ動く!
# 動的リンクの場合
$ scp hello server:/usr/local/bin/
$ ssh server /usr/local/bin/hello
# → エラー: ライブラリが見つからない
# → 依存ライブラリもインストール必要...
2. クロスコンパイルが簡単
# Linux用にビルド
GOOS=linux GOARCH=amd64 go build -o hello-linux
# Windows用にビルド
GOOS=windows GOARCH=amd64 go build -o hello.exe
# macOS用にビルド
GOOS=darwin GOARCH=amd64 go build -o hello-mac
すべて1つのコマンドで、依存関係の心配なし!
3. 予測可能な動作
静的リンク:
→ いつでも、どこでも、同じ動作
動的リンク:
→ システムのライブラリバージョンに依存
→ 「私の環境では動くのに...」問題
バイナリサイズを小さくする方法
方法1: デバッグ情報を削除
# 通常のビルド
go build -o hello hello.go
ls -lh hello
# → 1.8MB
# デバッグ情報を削除
go build -ldflags="-w -s" -o hello hello.go
ls -lh hello
# → 1.2MB (約30%削減!)
フラグの意味:
-w
: DWARF デバッグ情報を削除-s
: シンボルテーブルを削除
注意: これらを削除すると:
- デバッガでのデバッグが困難に
- スタックトレースの情報が減る
- でも、実行には影響なし
方法2: UPXで圧縮
# UPXをインストール
# (Linux) sudo apt install upx
# (macOS) brew install upx
# 圧縮
upx --best hello
ls -lh hello
# → 500KB (さらに半分以下!)
注意:
- 起動時に展開されるため、若干遅くなる
- アンチウイルスに誤検知されることがある
方法3: 不要なパッケージを使わない
// ❌ 大きいライブラリ
import "fmt"
func main() {
fmt.Println("Hello")
}
// → 1.8MB
// ✅ 軽量な方法
import "os"
func main() {
os.Stdout.WriteString("Hello\n")
}
// → 1.1MB (約40%削減!)
ただし、可読性とのトレードオフです。
実際の比較
プログラム例
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
各種ビルド方法の比較
ビルド方法 | サイズ | 備考 |
---|---|---|
通常 | 1.8 MB | デバッグ情報あり |
-ldflags="-w" | 1.5 MB | DWARF削除 |
-ldflags="-s" | 1.6 MB | シンボル削除 |
-ldflags="-w -s" | 1.2 MB | 両方削除 |
UPX圧縮後 | 500 KB | 実行時展開 |
C言語との比較まとめ
言語 | サイズ | 依存 | デプロイ |
---|---|---|---|
C (動的) | 16 KB | あり | 複雑 |
C (静的) | 750 KB | なし | 簡単 |
Go (通常) | 1.8 MB | なし | 簡単 |
Go (最適化) | 1.2 MB | なし | 簡単 |
Goのバイナリに含まれるもの
┌────────────────────────────┐
│ あなたのコード (50KB) │
├────────────────────────────┤
│ 標準ライブラリ (200KB) │
│ - fmt パッケージ │
│ - その他使用したパッケージ │
├────────────────────────────┤
│ Goランタイム (1MB) │
│ - スケジューラ │
│ - GC │
│ - チャネル │
│ - その他 │
├────────────────────────────┤
│ 型情報 (300KB) │
│ - リフレクション用 │
│ - 型チェック用 │
├────────────────────────────┤
│ デバッグ情報 (300KB) │
│ - DWARF │
│ - シンボルテーブル │
└────────────────────────────┘
合計: 約1.8MB
実践: サイズを確認してみよう
ステップ1: プログラムを作成
// hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
ステップ2: 通常ビルド
go build -o hello hello.go
ls -lh hello
file hello
ステップ3: 最適化ビルド
go build -ldflags="-w -s" -o hello-optimized hello.go
ls -lh hello-optimized
ステップ4: 比較
du -h hello hello-optimized
サイズが問題になる場合
ケース1: 組み込みシステム
RAM: 512MB
ストレージ: 4GB
↓
1.8MBは許容範囲内!
最近の組み込みシステムなら、通常問題なし。
ケース2: コンテナイメージ
# Dockerfile
FROM scratch
COPY hello /hello
ENTRYPOINT ["/hello"]
docker images
# hello latest 123abc 2MB
Goの静的リンクはコンテナと相性抜群!
ケース3: サーバーレス(AWS Lambda等)
制限: デプロイパッケージ 250MB
↓
1.8MBは全く問題なし!
よくある誤解
誤解1: 「Goは遅い、バイナリが大きいから」
真実:
- バイナリサイズと実行速度は無関係
- Goは高速(C/C++に匹敵)
誤解2: 「プログラムが大きくなるほど、バイナリも増大」
真実:
# 10行のプログラム
go build simple.go # → 1.8MB
# 10000行のプログラム
go build complex.go # → 2.5MB
# ランタイムが支配的なので、
# コードが増えても大幅には増えない
誤解3: 「本番環境では使えない」
真実:
- Docker、Google、Uber、など大企業で使用
- マイクロサービスに最適
- クラウドネイティブの標準
まとめ
なぜGoのバイナリは大きい?
- 🎒 Goランタイムを含む
- ゴルーチン、GC、など
- 📊 型情報を含む
- リフレクション、型チェック用
- 🐛 デバッグ情報を含む
- スタックトレース用
- 📦 静的リンク
- 依存ライブラリを全て含む
これは悪いこと?
いいえ! これは設計上の選択:
✅ 利点:
- デプロイが簡単(単一ファイル)
- 依存関係の問題なし
- クロスコンパイルが容易
- どこでも動く
❌ 欠点:
- ファイルサイズが大きい(1-2MB)
サイズを減らす方法
# 1. デバッグ情報削除(推奨)
go build -ldflags="-w -s"
# 2. 不要なimportを削除
# コードレビューで確認
# 3. UPX圧縮(必要なら)
upx --best binary
実用上の考え方
現代の環境では:
- 📱 ディスク: TB単位
- 💾 メモリ: GB単位
- 🌐 ネットワーク: 高速
1-2MBは問題にならない!
むしろ:
- ✅ デプロイの簡単さ
- ✅ 依存関係の無さ
- ✅ 予測可能な動作
これらの利点の方が、はるかに価値があります!
Goの哲学は「シンプルで予測可能」です。バイナリサイズが少し大きくても、開発・デプロイ・運用が簡単になることの方が、現代のソフトウェア開発では重要なのです!
おわりに
本日は、Go言語のよくある質問について解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント