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

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

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

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

スポンサーリンク

背景

Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。

Implementation

コンパイラをビルドするためにどのようなコンパイラ技術が使われていますか?

Goにはいくつかの本番用コンパイラがあり、さまざまなプラットフォーム向けに開発中のものも多数あります。

デフォルトのコンパイラgcは、goコマンドのサポートの一部としてGo配布物に含まれています。Gcは元々Cで書かれていました。これはブートストラッピングの困難さのためです。Go環境をセットアップするにはGoコンパイラが必要になってしまいます。しかし、物事は進歩し、Go 1.5リリース以降、コンパイラはGoプログラムになっています。コンパイラは、この設計文書とトークで説明されているように、自動翻訳ツールを使用してCからGoに変換されました。したがって、コンパイラは現在「自己ホスティング」されています。つまり、ブートストラッピング問題に直面する必要がありました。解決策は、通常動作するCインストールがあるのと同様に、すでに動作するGoインストールを用意しておくことです。ソースから新しいGo環境を立ち上げる方法の話は、こことここで説明されています。

Gcは再帰下降パーサーを使用してGoで書かれており、カスタムローダー(これもGoで書かれていますが、Plan 9ローダーに基づいています)を使用してELF/Mach-O/PEバイナリを生成します。

Gccgoコンパイラは、標準のGCCバックエンドに結合された再帰下降パーサーを持つC++で書かれたフロントエンドです。実験的なLLVMバックエンドは同じフロントエンドを使用しています。

プロジェクトの初期に、gcにLLVMを使用することを検討しましたが、パフォーマンス目標を満たすには大きすぎて遅すぎると判断しました。振り返ってより重要なのは、LLVMから始めていたら、スタック管理など、Goが必要とするがC標準セットアップの一部ではないABIおよび関連する変更の一部を導入することが難しくなっていただろうということです。

Goはもともとの目標ではありませんでしたが、Goコンパイラを実装するための素晴らしい言語であることが判明しました。最初から自己ホスティングではなかったことで、Goの設計は本来のユースケースであるネットワークサーバーに集中できました。もし早い段階でGoが自分自身をコンパイルすべきだと決めていたら、コンパイラ構築により適した言語になってしまっていたかもしれません。それは価値のある目標ですが、当初の目標ではありませんでした。

gcは独自の実装を持っていますが、ネイティブのレキサーとパーサーがgo/parserパッケージで利用でき、ネイティブの型チェッカーもあります。gcコンパイラはこれらのライブラリの変種を使用しています。

解説

この問題は何について説明しているの?

Goのコンパイラ自体が何で作られているかという、少しメタな話題です。つまり:

  • Goのプログラムをコンパイルするツールは、何の言語で書かれているの?
  • なぜその選択をしたの?

基本的な用語

  • コンパイラ: プログラミング言語を機械語に変換するプログラム
  • ブートストラッピング: 「自分自身で自分を持ち上げる」問題(後述)
  • 自己ホスティング: コンパイラが自分自身の言語で書かれていること
  • パーサー: ソースコードを解析して構造を理解するプログラム
  • フロントエンド: コンパイラの前半部分(コードの解析)
  • バックエンド: コンパイラの後半部分(機械語の生成)

Goのコンパイラの種類

1. gc (デフォルト)
go build main.go  # これが gc コンパイラを使う

特徴:

  • Goに標準で付いてくる
  • 最も広く使われている
  • 現在はGoで書かれている
2. Gccgo
go build -compiler=gccgo main.go

特徴:

  • GCCプロジェクトの一部
  • C++で書かれている
  • 異なる最適化
3. その他

開発中のものも含め、様々なプラットフォーム向けのコンパイラが存在します。

ブートストラッピング問題とは?

鶏と卵の問題

想像してください:

質問: Goのコンパイラを作るには?
答え: Goで書けばいい!

質問: でも、Goのコードをコンパイルするには?
答え: Goのコンパイラが必要...

質問: そのGoのコンパイラを作るには?
答え: Goで書けばいい!
(無限ループ!)

これがブートストラッピング問題です。

靴紐の比喩

“Bootstrap”は「靴紐で自分を持ち上げる」という意味で、不可能なことの例えです。

┌─────────┐
│  自分   │
└─────────┘
    │
    │ 靴紐を引っ張って
    │ 自分を持ち上げる?
    ▼
┌─────────┐
│  ???   │  ← 不可能!
└─────────┘

Goコンパイラの歴史

Phase 1: 最初はC言語で (Go 1.0 – 1.4)
C言語 → Goコンパイラ → Goプログラム
(既にある)  (C製)     (コンパイル可能!)

理由:

  • C言語のコンパイラはすでに存在する
  • ブートストラッピング問題を回避

問題:

  • メンテナンスが大変
  • Goの機能をフル活用できない
Phase 2: Goで書き直す (Go 1.5以降)
Goコンパイラ(旧版) → Goコンパイラ(新版) → Goプログラム
(Go 1.4, C製)      (Go製!)           (コンパイル可能!)

方法:

  1. C言語版のコンパイラ(Go 1.4)を使う
  2. 新しいGoで書かれたコンパイラをコンパイル
  3. 以降はGoのコンパイラがGoのコンパイラをコンパイル

変換方法

C言語からGoへの変換は自動翻訳ツールで実施:

// C言語版(旧)
void compile(char* source) {
    // コンパイル処理
}

↓ 自動変換

// Go版(新)
func compile(source string) {
    // コンパイル処理
}

gcコンパイラの技術詳細

再帰下降パーサー

パーサーは、コードを読んで構造を理解します:

// このコード
func add(a, b int) int {
    return a + b
}

// パーサーが理解
関数:
  名前: add
  引数: a(int), b(int)
  返り値: int
  本体:
    return文: a + b

再帰下降は、コードを上から下に読み進める方式です。

カスタムローダー

コンパイル後、実行可能ファイルを生成:

Goコード → コンパイラ → 機械語 → ローダー → 実行ファイル
                                         (.exe, ELFなど)

対応フォーマット:

  • ELF (Linux)
  • Mach-O (macOS)
  • PE (Windows)

なぜLLVMを使わなかったのか?

LLVMとは?

LLVMは強力なコンパイラ基盤技術:

  • Clang (C/C++コンパイラ)が使用
  • Rust、Swiftなども使用
Goが採用しなかった理由
理由1: パフォーマンス
LLVM: 高機能だが重い・遅い
  ↓
Goの目標: 高速コンパイル
  ↓
判断: カスタム実装の方が速い

具体例:

# Goのコンパイルは非常に速い
$ time go build main.go
real    0m0.150s  # 0.15秒!

# もしLLVMベースだったら...
# (仮想的な例)
real    0m2.500s  # 2.5秒...
理由2: Goの特殊な要件

Goには独自の機能があります:

// ゴルーチン(軽量スレッド)
go func() {
    // 並行処理
}()

// 独自のスタック管理
// 自動的に伸縮するスタック

これらは標準的なC/C++にはない機能です。 LLVMを使うと、これらの実装が困難でした。

Goでコンパイラを書くメリット

当初の目標ではなかった
最初の目標: ネットワークサーバーを書きやすく
  ↓
結果: コンパイラも書きやすい言語だった!
もし最初からコンパイラを目標にしていたら?
コンパイラ構築に特化した言語
  ↓
でも、ネットワークサーバーには使いにくい?
  ↓
本来の目標から外れる

Goの利点

// 並行処理が簡単
go parseFile(filename)

// メモリ安全
// ガベージコレクション

// 標準ライブラリが豊富
import "go/parser"
import "go/ast"

実際のコンパイル過程

段階1: 字句解析 (Lexer)
code := "x := 42"

// トークンに分解
tokens := []Token{
    {Type: IDENT, Value: "x"},
    {Type: ASSIGN, Value: ":="},
    {Type: NUMBER, Value: "42"},
}
段階2: 構文解析 (Parser)
// 構文木を作成
tree := &AssignStmt{
    Left:  &Ident{Name: "x"},
    Right: &BasicLit{Value: 42},
}
段階3: 型チェック
// 型の整合性を確認
x := 42      // OK: int
y := "text"  // OK: string
z := x + y   // エラー: 型が合わない!
段階4: コード生成
構文木 → 中間表現 → 機械語 → 実行ファイル

標準ライブラリのパーサー

Goには自分自身のコードを解析するライブラリがあります!

go/parserパッケージ
package main

import (
    "fmt"
    "go/parser"
    "go/token"
)

func main() {
    // Goコードを解析
    src := `package main
    
    func add(a, b int) int {
        return a + b
    }`
    
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "", src, 0)
    if err != nil {
        panic(err)
    }
    
    // 関数名を表示
    for _, decl := range f.Decls {
        fmt.Println("Found declaration")
    }
}

使用例:

  • コード生成ツール
  • リンター(コード品質チェック)
  • リファクタリングツール
  • IDEの機能
go/astパッケージ

AST (Abstract Syntax Tree): 抽象構文木

import "go/ast"

// コードの構造を操作できる
ast.Inspect(node, func(n ast.Node) bool {
    // 全てのノードを走査
    return true
})

実践例: 簡単なツールを作る

関数名を抽出するツール
package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
    "log"
)

func main() {
    src := `
    package example
    
    func Hello() {}
    func World() {}
    func Add(a, b int) int { return a + b }
    `
    
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "", src, 0)
    if err != nil {
        log.Fatal(err)
    }
    
    // 全ての関数を探す
    for _, decl := range f.Decls {
        // 関数宣言の場合
        if fn, ok := decl.(*ast.FuncDecl); ok {
            fmt.Printf("関数: %s\n", fn.Name.Name)
        }
    }
}

出力:

関数: Hello
関数: World
関数: Add

コンパイラの仕組みを学ぶリソース

標準ライブラリ
import "go/scanner"    // 字句解析
import "go/parser"     // 構文解析
import "go/ast"        // 抽象構文木
import "go/token"      // トークン
import "go/types"      // 型チェック
簡単な実験
package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    // 簡単な式を解析
    expr := "2 + 3 * 4"
    
    // 式として解析
    node, err := parser.ParseExpr(expr)
    if err != nil {
        panic(err)
    }
    
    // 構造を表示
    ast.Print(token.NewFileSet(), node)
}

まとめ

Goコンパイラの歴史
  1. 🔧 初期 (C言語製)
    • ブートストラッピング問題を回避
    • 実用的なアプローチ
  2. 🚀 現在 (Go製)
    • 自己ホスティング
    • Goの機能をフル活用
技術的な選択
  • LLVM不採用: 速度とGoの特殊機能のため
  • カスタム実装: 高速コンパイル
  • 再帰下降パーサー: シンプルで効率的
Goの強み
  • 🎯 本来の目標: ネットワークサーバー
  • 💡 予想外の強み: コンパイラ構築にも適していた
  • 📚 標準ライブラリ: 自己解析機能が充実
あなたにできること
  1. go/parserを使ってみる
    • 自分のコードを解析
    • ツールを作成
  2. ASTを学ぶ
    • コードの構造を理解
    • 静的解析ツールを作る
  3. コンパイラの仕組みを学ぶ
    • より深いGo理解につながる

GoのコンパイラがGoで書かれているという事実は、Goの成熟度と表現力の証明です。これは、Goが単なるシステムプログラミング言語ではなく、複雑なソフトウェアを構築できる強力な言語であることを示しています!

おわりに 

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

よっしー
よっしー

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

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

コメント

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