Go言語入門:よくある質問 -Implementation Vol.3-

スポンサーリンク
Go言語入門:よくある質問 -Implementation Vol.3- ノウハウ
Go言語入門:よくある質問 -Implementation Vol.3-
この記事は約13分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のよくある質問 について解説しています。

スポンサーリンク
  1. 背景
  2. Implementation
    1. なぜ些細なプログラムのバイナリがこんなに大きいのか?
    2. 解説
        1. この問題は何について説明しているの?
        2. 基本的な用語
        3. 実際のサイズを見てみよう
          1. Hello Worldプログラム
          2. C言語との比較
        4. なぜGoのバイナリは大きいのか?
          1. 理由1: Goランタイムが含まれる
          2. 理由2: 型情報が含まれる
          3. 理由3: デバッグ情報
        5. 静的リンク vs 動的リンク
          1. 静的リンク(Goのデフォルト)
          2. 動的リンク(C言語の一般的な方法)
        6. Goが静的リンクを選んだ理由
          1. 1. デプロイの簡単さ
          2. 2. クロスコンパイルが簡単
          3. 3. 予測可能な動作
        7. バイナリサイズを小さくする方法
          1. 方法1: デバッグ情報を削除
          2. 方法2: UPXで圧縮
          3. 方法3: 不要なパッケージを使わない
        8. 実際の比較
          1. プログラム例
          2. 各種ビルド方法の比較
          3. C言語との比較まとめ
        9. Goのバイナリに含まれるもの
        10. 実践: サイズを確認してみよう
          1. ステップ1: プログラムを作成
          2. ステップ2: 通常ビルド
          3. ステップ3: 最適化ビルド
          4. ステップ4: 比較
        11. サイズが問題になる場合
          1. ケース1: 組み込みシステム
          2. ケース2: コンテナイメージ
          3. ケース3: サーバーレス(AWS Lambda等)
        12. よくある誤解
          1. 誤解1: 「Goは遅い、バイナリが大きいから」
          2. 誤解2: 「プログラムが大きくなるほど、バイナリも増大」
          3. 誤解3: 「本番環境では使えない」
        13. まとめ
          1. なぜGoのバイナリは大きい?
          2. これは悪いこと?
          3. サイズを減らす方法
          4. 実用上の考え方
  3. おわりに 

背景

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 MBDWARF削除
-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のバイナリは大きい?
  1. 🎒 Goランタイムを含む
    • ゴルーチン、GC、など
  2. 📊 型情報を含む
    • リフレクション、型チェック用
  3. 🐛 デバッグ情報を含む
    • スタックトレース用
  4. 📦 静的リンク
    • 依存ライブラリを全て含む
これは悪いこと?

いいえ! これは設計上の選択:

利点:

  • デプロイが簡単(単一ファイル)
  • 依存関係の問題なし
  • クロスコンパイルが容易
  • どこでも動く

欠点:

  • ファイルサイズが大きい(1-2MB)
サイズを減らす方法
# 1. デバッグ情報削除(推奨)
go build -ldflags="-w -s"

# 2. 不要なimportを削除
# コードレビューで確認

# 3. UPX圧縮(必要なら)
upx --best binary
実用上の考え方

現代の環境では:

  • 📱 ディスク: TB単位
  • 💾 メモリ: GB単位
  • 🌐 ネットワーク: 高速

1-2MBは問題にならない!

むしろ:

  • ✅ デプロイの簡単さ
  • ✅ 依存関係の無さ
  • ✅ 予測可能な動作

これらの利点の方が、はるかに価値があります!

Goの哲学は「シンプルで予測可能」です。バイナリサイズが少し大きくても、開発・デプロイ・運用が簡単になることの方が、現代のソフトウェア開発では重要なのです!

おわりに 

本日は、Go言語のよくある質問について解説しました。

よっしー
よっしー

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

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました