第2章: 環境構築とツールチェーン
学習目標
この章を終えると、以下ができるようになります:
- Goの開発環境を正しくセットアップできる
- go コマンドの基本的な使い方を理解できる
- モジュールシステム(go modules)を使ってプロジェクトを管理できる
- Goの標準ツール(fmt, vet, testなど)を活用できる
- コンパイラとリンカの動作を理解できる
- 実行ファイルの生成プロセスを機械レベルで理解できる
Goのインストール - 深層解説
🔑 なぜGoのインストールが特別なのか
他の言語と異なり、Goは完全に自己完結したツールチェーンを提供します。これには深い理由があります:
従来の言語の問題点:
C/C++の場合:
┌─────────────────────────────────────────┐
│ コンパイラ (gcc/clang) │ → 別々にインストール
│ リンカ (ld) │ → バージョン依存
│ ビルドツール (make/cmake) │ → 設定が複雑
│ パッケージマネージャ (apt/brew) │ → プラットフォーム依存
└─────────────────────────────────────────┘
↓ 結果
環境差異が大きい、ビルドが失敗しやすい
Goのアプローチ:
Goの場合:
┌─────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────┐ │
│ │ go コマンド (全てを統合) │ │
│ ├─────────────────────────────────┤ │
│ │ • コンパイラ (gc) │ │
│ │ • リンカ (link) │ │
│ │ • ビルドツール │ │
│ │ • パッケージマネージャ │ │
│ │ • フォーマッタ │ │
│ │ • テストランナー │ │
│ │ • 静的解析ツール │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
↓ 結果
一度インストールすれば全て揃う
公式サイトからのインストール - 内部動作
macOS
# Homebrewを使用
brew install go
💡 Homebrewインストール時の内部動作:
1. ダウンロード
↓
https://go.dev/dl/ から最新バイナリを取得
2. 検証
┌──────────────────────────────────────┐
│ SHA256チェックサム検証 │
│ ファイル破損・改ざん検知 │
└──────────────────────────────────────┘
3. 展開
/usr/local/Cellar/go/X.Y.Z/ に配置
4. シンボリックリンク作成
/usr/local/bin/go → /usr/local/Cellar/go/X.Y.Z/bin/go
5. 環境変数設定
PATH に /usr/local/bin を追加(自動)
ディレクトリ構造:
/usr/local/Cellar/go/1.21.5/
├── bin/
│ ├── go ← メインコマンド
│ ├── gofmt ← フォーマッタ
│ └── godoc ← ドキュメントツール
├── libexec/
│ ├── bin/
│ │ ├── compile ← コンパイラ
│ │ ├── link ← リンカ
│ │ └── asm ← アセンブラ
│ └── pkg/
│ └── tool/
│ └── darwin_amd64/ ← プラットフォーム固有ツール
├── pkg/
│ └── darwin_amd64/ ← プリコンパイル済み標準ライブラリ
└── src/
└── ... ← 標準ライブラリのソースコード
Linux - 手動インストール詳細
# 1. tarballをダウンロード
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
# 2. 展開(既存のGoを削除)
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
# 3. PATHに追加(~/.bashrcまたは~/.zshrcに追記)
export PATH=$PATH:/usr/local/go/bin
🔑 なぜ既存のGoを削除するのか:
問題: 古いGoと新しいGoが混在する場合
/usr/local/go/
├── bin/
│ └── go ← バージョン1.20
└── pkg/
└── linux_amd64/ ← 1.20用のライブラリ
新しいGoをインストール後:
├── bin/
│ └── go ← バージョン1.21(上書き)
└── pkg/
├── linux_amd64/ ← 1.20のライブラリ(残留)
└── linux_amd64_1.21/ ← 1.21のライブラリ(新規)
結果: リンク時にバージョン不整合でエラー
解決: rm -rf で完全削除してからインストール
Windows - MSIインストーラの内部動作
# 1. https://go.dev/dl/ から.msiファイルをダウンロード
# 2. インストーラーを実行
# 3. 自動的にPATHが設定されます
💡 MSIインストーラが行うこと:
1. ファイルコピー
C:\Program Files\Go\ にバイナリ配置
2. レジストリ設定
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
├── GOROOT = C:\Program Files\Go
└── Path += C:\Program Files\Go\bin
3. ユーザー環境変数
HKEY_CURRENT_USER\Environment
└── GOPATH = %USERPROFILE%\go
4. 再起動不要でPATH有効化
ブロードキャストメッセージ送信
WM_SETTINGCHANGE → 全プロセスに通知
インストールの確認 - 深層動作
# Goのバージョン確認
go version
# 出力例: go version go1.21.5 darwin/amd64
🔑 go version コマンドの内部動作:
// Go自身のソースコード(疑似コード)
func versionCommand() {
// ビルド時に埋め込まれた情報を取得
version := runtime.Version() // "go1.21.5"
goos := runtime.GOOS // "darwin"
goarch := runtime.GOARCH // "amd64"
fmt.Printf("go version %s %s/%s\n", version, goos, goarch)
}
runtime.Version() の実装:
コンパイル時:
1. バージョン情報を定数として埋め込み
const TheVersion = "go1.21.5"
2. バイナリの .rodata セクションに配置
実行時:
1. .rodata セクションから文字列読み取り
┌─────────────────────────────────┐
│ ELF/Mach-O/PE バイナリ構造 │
├─────────────────────────────────┤
│ .text (コード) │
│ .rodata (読み取り専用データ) │ ← ここに "go1.21.5"
│ .data (初期化済みデータ) │
│ .bss (未初期化データ) │
└─────────────────────────────────┘
2. 文字列を返す(メモリコピー不要)
# インストールパスの確認
which go
# 出力例: /usr/local/go/bin/go
💡 which コマンドの動作原理:
1. 環境変数 PATH を取得
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin
2. コロン (:) で分割
paths = ["/usr/local/bin", "/usr/bin", "/bin", ...]
3. 各ディレクトリで "go" を検索
for each path in paths:
fullpath = path + "/go"
if file_exists(fullpath) and is_executable(fullpath):
print(fullpath)
return
4. システムコール stat() で実行可能性を確認
syscall.Stat("/usr/local/go/bin/go") → mode & 0111 != 0
環境変数の理解 - 機械レベルの詳細
重要な環境変数
# すべての環境変数を表示
go env
# 特定の変数を確認
go env GOPATH
go env GOROOT
go env GOOS
go env GOARCH
🔑 go env コマンドの内部動作:
// go env の疑似実装
func envCommand(args []string) {
// 1. ビルトイン値を取得
goroot := runtime.GOROOT() // /usr/local/go
gopath := defaultGOPATH() // ~/go
// 2. 環境変数で上書き
if val := os.Getenv("GOROOT"); val != "" {
goroot = val
}
if val := os.Getenv("GOPATH"); val != "" {
gopath = val
}
// 3. 計算値を追加
gocache := gopath + "/pkg/mod/cache"
gomodcache := gopath + "/pkg/mod"
// 4. 出力
fmt.Printf("GOROOT=%s\n", goroot)
fmt.Printf("GOPATH=%s\n", gopath)
// ...
}
主要な環境変数の詳細
| 変数 | 説明 | デフォルト値 | メモリ上の役割 |
|---|---|---|---|
| GOROOT | Goのインストールディレクトリ | /usr/local/go | 標準ライブラリの検索パス |
| GOPATH | ワークスペースのルート | ~/go | モジュールキャッシュとビルド成果物 |
| GOBIN | 実行ファイルのインストール先 | $GOPATH/bin | go install の出力先 |
| GOOS | ターゲットOS | 現在のOS | クロスコンパイル用 |
| GOARCH | ターゲットアーキテクチャ | 現在のアーキテクチャ | 命令セット選択 |
| GO111MODULE | モジュールモード | on | モジュール解決方法 |
| GOCACHE | ビルドキャッシュ | ~/.cache/go-build | 高速リビルド |
GOROOT - 標準ライブラリの心臓部
GOROOT=/usr/local/go
構造:
/usr/local/go/
├── src/ ← 標準ライブラリのソースコード
│ ├── fmt/
│ │ ├── print.go
│ │ ├── scan.go
│ │ └── format.go
│ ├── os/
│ ├── net/
│ └── ...
├── pkg/ ← プリコンパイル済みライブラリ
│ └── darwin_amd64/
│ ├── fmt.a ← アーカイブファイル(オブジェクトコード)
│ ├── os.a
│ └── ...
└── bin/
└── go
コンパイル時の動作:
1. import "fmt" を発見
2. GOROOT/src/fmt を検索
3. GOROOT/pkg/darwin_amd64/fmt.a が存在すれば使用
4. なければ GOROOT/src/fmt/*.go からコンパイル
💡 プリコンパイル済みライブラリ (.a ファイル) の中身:
fmt.a ファイルの構造:
┌──────────────────────────────────────┐
│ アーカイブヘッダー │
├──────────────────────────────────────┤
│ オブジェクトファイル 1: print.o │
│ ┌──────────────────────────────────┐ │
│ │ .text セクション │ │ ← 機械語コード
│ │ Println 関数の機械語 │ │
│ │ Printf 関数の機械語 │ │
│ ├──────────────────────────────────┤ │
│ │ .rodata セクション │ │ ← 定数データ
│ │ フォーマット文字列 │ │
│ ├──────────────────────────────────┤ │
│ │ .data セクション │ │ ← 初期化済み変数
│ ├──────────────────────────────────┤ │
│ │ シンボルテーブル │ │ ← 関数名とアドレス
│ │ fmt.Println → 0x1234 │ │
│ │ fmt.Printf → 0x5678 │ │
│ └──────────────────────────────────┘ │
├──────────────────────────────────────┤
│ オブジェクトファイル 2: scan.o │
│ ... │
└──────────────────────────────────────┘
リンク時:
1. シンボルテーブルから必要な関数を検索
2. 機械語コードを最終バイナリにコピー
3. アドレスを再配置(リロケーション)
GOPATH - モジュール時代の役割
GOPATH=~/go
構造:
~/go/
├── bin/ ← go install で生成された実行ファイル
│ ├── golangci-lint
│ ├── gopls
│ └── dlv
├── pkg/
│ ├── mod/ ← ダウンロードした外部モジュール
│ │ ├── cache/ ← モジュールキャッシュ
│ │ │ └── download/
│ │ │ └── github.com/
│ │ │ └── gorilla/
│ │ │ └── mux/
│ │ │ └── @v/
│ │ │ ├── v1.8.0.zip
│ │ │ ├── v1.8.0.mod
│ │ │ └── v1.8.0.info
│ │ └── github.com/
│ │ └── gorilla/
│ │ └── mux@v1.8.0/ ← 展開済みソース
│ └── sumdb/ ← チェックサムデータベース
└── src/ ← レガシー(Go Modules前)
🔑 モジュールキャッシュの内部構造:
モジュールの保存形式:
github.com/gorilla/mux@v1.8.0.zip
↓ 展開
$GOPATH/pkg/mod/github.com/gorilla/mux@v1.8.0/
├── go.mod
├── go.sum
├── mux.go
├── route.go
└── ...
ビルド時:
1. go.mod で依存を検出
require github.com/gorilla/mux v1.8.0
2. キャッシュを確認
stat($GOPATH/pkg/mod/github.com/gorilla/mux@v1.8.0)
3. なければダウンロード
https://proxy.golang.org/github.com/gorilla/mux/@v/v1.8.0.zip
↓
$GOPATH/pkg/mod/cache/download/...
↓
展開して $GOPATH/pkg/mod/... に配置
4. チェックサム検証
go.sum の SHA256 と比較
GOOS と GOARCH - クロスコンパイルの秘密
# 現在のプラットフォーム
go env GOOS GOARCH
# 出力例: darwin amd64
# 利用可能な組み合わせ
go tool dist list
💡 サポートされるプラットフォーム:
GOOS の値:
- darwin (macOS)
- linux (Linux)
- windows (Windows)
- freebsd (FreeBSD)
- openbsd (OpenBSD)
- android (Android)
- ios (iOS)
...
GOARCH の値:
- amd64 (x86-64)
- arm64 (ARM 64-bit)
- arm (ARM 32-bit)
- 386 (x86 32-bit)
- mips (MIPS)
- wasm (WebAssembly)
...
組み合わせ例:
linux/amd64 ← 最も一般的なサーバー
linux/arm64 ← Raspberry Pi 4, AWS Graviton
darwin/amd64 ← Intel Mac
darwin/arm64 ← M1/M2 Mac
windows/amd64 ← Windows PC
js/wasm ← ブラウザ
🔑 クロスコンパイル時のコード生成:
// 例: 同じGoコード
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
// darwin/amd64 でコンパイル
GOOS=darwin GOARCH=amd64 go build -o hello_mac
// linux/amd64 でコンパイル
GOOS=linux GOARCH=amd64 go build -o hello_linux
生成される機械語の違い:
darwin/amd64 (Mach-O形式):
┌──────────────────────────────────────┐
│ Mach-O ヘッダー │
│ - マジックナンバー: 0xFEEDFACF │
│ - CPU_TYPE: x86_64 │
│ - CPU_SUBTYPE: ALL │
├──────────────────────────────────────┤
│ ロードコマンド │
│ - LC_SEGMENT_64 (__TEXT) │
│ - LC_SEGMENT_64 (__DATA) │
│ - LC_DYLD_INFO_ONLY │
├──────────────────────────────────────┤
│ __TEXT セグメント │
│ __text セクション │
│ 機械語コード (x86-64) │
│ syscall: int 0x2000004 (write) │ ← macOS システムコール番号
├──────────────────────────────────────┤
│ __DATA セグメント │
│ 文字列 "Hello\n" │
└──────────────────────────────────────┘
linux/amd64 (ELF形式):
┌──────────────────────────────────────┐
│ ELF ヘッダー │
│ - マジックナンバー: 0x7F 'E' 'L' 'F' │
│ - クラス: 64-bit │
│ - エンディアン: リトル │
├──────────────────────────────────────┤
│ プログラムヘッダー │
│ - PT_LOAD (実行可能) │
│ - PT_LOAD (読み書き可能) │
├──────────────────────────────────────┤
│ .text セクション │
│ 機械語コード (x86-64) │
│ syscall: syscall (番号1, write) │ ← Linux システムコール番号
├──────────────────────────────────────┤
│ .rodata セクション │
│ 文字列 "Hello\n" │
└──────────────────────────────────────┘
重要な違い:
1. バイナリフォーマット (Mach-O vs ELF)
2. システムコール番号 (OS依存)
3. ABIの違い (関数呼び出し規約)
4. 動的リンカのパス
GOPATHの構造(レガシー) - なぜ廃れたのか
Go 1.11以前は、GOPATHベースの開発が主流でした:
$GOPATH/
├── bin/ # 実行ファイル
├── pkg/ # コンパイル済みパッケージ
└── src/ # ソースコード
└── github.com/
└── username/
└── project/
⚠️ GOPATH方式の問題点:
問題1: グローバル名前空間
┌──────────────────────────────────────┐
│ $GOPATH/src/ │
├──────────────────────────────────────┤
│ github.com/user1/mylib/ │
│ ├── v1.0.0 のコード │
│ └── 複数バージョン共存不可! │
│ │
│ github.com/user2/app/ │
│ ├── mylib v1.0.0 に依存 │
│ └── 別プロジェクトは v2.0.0 が必要 │
│ → 両立不可能! │
└──────────────────────────────────────┘
問題2: プロジェクトの配置場所が固定
開発者のコード:
/Users/alice/project/ ← ここで開発したい
↓ 不可能
必ず $GOPATH/src/github.com/alice/project/ に配置必須
問題3: 依存バージョン管理が不可能
go get でダウンロード
↓
常に最新版を取得
↓
バージョン固定の仕組みなし
↓
ビルドが再現不可能
現在(Go Modules時代): プロジェクトをどこにでも配置可能
~/projects/
└── myapp/
├── go.mod ← このファイルがあればモジュール
├── go.sum ← 依存のチェックサム
└── main.go
利点:
1. 任意の場所に配置可能
2. 各プロジェクトが独立した依存を持つ
3. バージョンが明示的 (go.mod)
4. 再現可能なビルド (go.sum)
Go Modulesの基本 - 内部メカニズム
モジュールとは - 概念の深層
Go Modulesは、依存関係を管理する公式システムです(Go 1.11で導入)。
モジュールの構成要素:
go.mod: モジュール名と依存関係を定義go.sum: 依存関係のチェックサム(整合性検証)
🔑 なぜ go.mod と go.sum の2ファイルが必要なのか:
go.mod の役割:
┌──────────────────────────────────────┐
│ module github.com/user/myapp │ ← モジュール名
│ │
│ go 1.21 │ ← Go バージョン
│ │
│ require ( │
│ github.com/gorilla/mux v1.8.0 │ ← 直接依存
│ github.com/lib/pq v1.10.9 │
│ ) │
│ │
│ require ( │
│ github.com/mattn/go-sqlite3 │ ← 間接依存
│ v1.14.18 // indirect │ (推移的依存)
│ ) │
└──────────────────────────────────────┘
go.sum の役割:
┌──────────────────────────────────────┐
│ github.com/gorilla/mux v1.8.0 │
│ h1:abc...xyz │ ← SHA256 チェックサム
│ github.com/gorilla/mux v1.8.0/go.mod │
│ h1:def...uvw │ ← go.mod のチェックサム
│ │
│ github.com/lib/pq v1.10.9 │
│ h1:ghi...rst │
│ ... │
└──────────────────────────────────────┘
なぜ2つ必要?
go.mod だけでは:
× バージョンは固定できる
× でもコードの内容は保証できない
例: v1.8.0 というタグは削除して再作成可能
→ 同じバージョン番号でも中身が変わる可能性
go.sum も使うことで:
○ コードの内容まで保証
○ 改ざん検出
○ 再現可能なビルド
新しいモジュールの作成 - 完全解説
# プロジェクトディレクトリを作成
mkdir myapp
cd myapp
# モジュールの初期化
go mod init github.com/username/myapp
# go.modファイルが生成される
💡 go mod init の内部動作:
1. カレントディレクトリを確認
pwd → /Users/alice/projects/myapp
2. 既存の go.mod をチェック
if exists("go.mod"):
error("go.mod already exists")
3. モジュールパスを決定
引数がある場合:
modulePath = args[0] // github.com/username/myapp
引数がない場合:
gitリモートから推測を試みる
git remote get-url origin
→ https://github.com/username/myapp.git
→ modulePath = github.com/username/myapp
4. Go バージョンを検出
goVersion = runtime.Version() // "1.21"
5. go.mod を生成
content = `module ` + modulePath + `\n\n`
content += `go ` + goVersion + `\n`
writeFile("go.mod", content)
6. 権限設定
chmod("go.mod", 0644) // rw-r--r--
生成されたgo.mod:
module github.com/username/myapp
go 1.21
🔑 go.mod の文法解析:
go.mod のパーサー動作:
入力:
module github.com/username/myapp
go 1.21
require (
github.com/gorilla/mux v1.8.0
)
字句解析:
TOKEN_MODULE "module"
TOKEN_STRING "github.com/username/myapp"
TOKEN_NEWLINE
TOKEN_GO "go"
TOKEN_VERSION "1.21"
...
構文解析:
AST (抽象構文木):
└─ ModFile
├─ Module
│ └─ Path: "github.com/username/myapp"
├─ Go
│ └─ Version: "1.21"
└─ Require
└─ Dependency
├─ Path: "github.com/gorilla/mux"
└─ Version: "v1.8.0"
内部表現:
type ModFile struct {
Module *Module
Go *Go
Require []*Require
}
依存関係の追加 - ダウンロードからリンクまで
// main.go
package main
import (
"fmt"
"github.com/fatih/color" // 外部パッケージ
)
func main() {
color.Green("Hello, Go Modules!")
}
# 依存関係を自動ダウンロード
go get github.com/fatih/color
# または、単にビルド/実行すると自動でダウンロード
go run main.go
🔑 go get の完全な内部動作:
go get github.com/fatih/color の実行フロー:
1. モジュールパスの解決
┌────────────────────────────────────┐
│ github.com/fatih/color を解析 │
├────────────────────────────────────┤
│ VCS: Git │
│ リポジトリ: github.com/fatih/color │
│ サブディレクトリ: なし │
└────────────────────────────────────┘
2. プロキシに問い合わせ (Go 1.13+)
GET https://proxy.golang.org/github.com/fatih/color/@v/list
応答:
v1.0.0
v1.1.0
...
v1.16.0
v1.17.0
3. 最新バージョンを選択
latest = v1.17.0 (セマンティックバージョニング)
4. モジュール情報を取得
GET https://proxy.golang.org/github.com/fatih/color/@v/v1.17.0.info
応答:
{
"Version": "v1.17.0",
"Time": "2023-11-10T10:00:00Z"
}
5. go.mod を取得
GET https://proxy.golang.org/github.com/fatih/color/@v/v1.17.0.mod
応答:
module github.com/fatih/color
go 1.13
require (
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.20
)
6. 推移的依存を解決
color が依存している:
├─ github.com/mattn/go-colorable v0.1.13
└─ github.com/mattn/go-isatty v0.0.20
これらも再帰的にダウンロード
7. ソースコードをダウンロード
GET https://proxy.golang.org/github.com/fatih/color/@v/v1.17.0.zip
↓ 保存先
$GOPATH/pkg/mod/cache/download/github.com/fatih/color/@v/v1.17.0.zip
8. ZIP を展開
unzip → $GOPATH/pkg/mod/github.com/fatih/color@v1.17.0/
9. チェックサムを計算
SHA256(v1.17.0.zip) → h1:abc...xyz
10. go.sum に記録
github.com/fatih/color v1.17.0 h1:abc...xyz
github.com/fatih/color v1.17.0/go.mod h1:def...uvw
11. go.mod を更新
require github.com/fatih/color v1.17.0
12. 依存グラフをメモリに構築
myapp
└── github.com/fatih/color v1.17.0
├── github.com/mattn/go-colorable v0.1.13
│ └── github.com/mattn/go-isatty v0.0.20
│ └── golang.org/x/sys v0.14.0
└── github.com/mattn/go-isatty v0.0.20
└── golang.org/x/sys v0.14.0
更新後のgo.mod:
module github.com/username/myapp
go 1.21
require github.com/fatih/color v1.16.0
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.14.0 // indirect
)
💡 // indirect コメントの意味:
require の種類:
直接依存 (コメントなし):
require github.com/fatih/color v1.16.0
↑ あなたのコードが import している
間接依存 (// indirect):
require github.com/mattn/go-colorable v0.1.13 // indirect
↑ あなたのコードは import していない
↑ 依存ライブラリが使用している
なぜ go.mod に書く?
依存解決の最小バージョンを記録
バージョン衝突を解決
依存関係の管理コマンド - 詳細動作
# 依存関係の追加
go get github.com/gorilla/mux@latest
# 特定バージョンを指定
go get github.com/gorilla/mux@v1.8.0
# 使われていない依存関係を削除
go mod tidy
# 依存関係をvendorディレクトリにコピー
go mod vendor
# 依存関係のグラフを表示
go mod graph
🔑 go mod tidy の動作原理:
go mod tidy の実行フロー:
1. ソースコードをスキャン
find . -name "*.go" -exec parse_imports {} \;
main.go:
import (
"fmt"
"github.com/fatih/color"
)
utils.go:
import (
"strings"
"github.com/gorilla/mux"
)
→ 使用パッケージ:
- fmt (標準ライブラリ)
- strings (標準ライブラリ)
- github.com/fatih/color
- github.com/gorilla/mux
2. go.mod の require を確認
require (
github.com/fatih/color v1.16.0
github.com/gorilla/mux v1.8.0
github.com/unused/package v1.0.0 ← コードで使われていない
)
3. 未使用の依存を削除
github.com/unused/package を削除
4. 不足している依存を追加
もし新しい import があれば自動追加
5. 推移的依存を解決
各依存の go.mod を読み取り
必要な // indirect 依存を追加
6. go.sum を更新
新しい依存のチェックサムを追加
削除された依存のエントリは保持(履歴として)
7. ファイルを書き戻す
go.mod と go.sum を保存
🔑 go mod vendor の詳細動作:
go mod vendor の実行フロー:
1. 依存関係グラフを構築
go.mod を解析 → 全依存をリストアップ
2. vendor/ ディレクトリを作成
mkdir -p vendor
3. 各依存をコピー
for each dependency:
src = $GOPATH/pkg/mod/github.com/gorilla/mux@v1.8.0
dst = ./vendor/github.com/gorilla/mux
copy_tree(src, dst)
4. modules.txt を生成
vendor/modules.txt:
# github.com/gorilla/mux v1.8.0
## explicit; go 1.13
github.com/gorilla/mux
github.com/gorilla/mux/middleware
5. ビルド時の動作
go build:
if exists("vendor/"):
use vendor instead of $GOPATH/pkg/mod
利点:
- リポジトリに全依存を含められる
- ネットワーク不要でビルド可能
- 依存が削除されても影響なし
欠点:
- リポジトリサイズが増大
- vendor/ の更新を忘れがち
goコマンドの基本 - コンパイルプロセスの詳細
ビルドと実行 - 機械語生成まで
# 直接実行(開発時に便利)
go run main.go
# 複数ファイル
go run main.go utils.go
# カレントディレクトリの全ファイル
go run .
# ビルド(バイナリ生成)
go build
# 出力ファイル名を指定
go build -o myapp
# 特定のファイルをビルド
go build main.go
# リリースビルド(最適化)
go build -ldflags="-s -w" -o myapp
🔑 go run の完全な実行フロー:
go run main.go の内部動作:
1. 一時ディレクトリを作成
tmpdir = /var/folders/xx/yy/T/go-build123456789/
2. ソースファイルをパース
parse("main.go") → AST (抽象構文木)
AST 例:
File {
Package: "main"
Imports: [
{Path: "fmt"}
]
Decls: [
FuncDecl {
Name: "main"
Body: [
ExprStmt {
Call {
Fun: "fmt.Println"
Args: ["Hello"]
}
}
]
}
]
}
3. 型チェック
type checker:
- 変数の型を推論
- 型の整合性を確認
- 未定義識別子を検出
4. コンパイル
compile main.go → main.o (オブジェクトファイル)
コンパイラの処理:
AST → SSA (Static Single Assignment) → 機械語
SSA 例:
v1 = ConstString "Hello"
v2 = Call fmt.Println v1
v3 = Return
機械語生成 (amd64):
0x1000: MOVQ $0x... %rax // 文字列アドレスを rax に
0x1008: MOVQ %rax, 0(%rsp) // スタックに積む
0x1010: CALL fmt.Println // 関数呼び出し
0x1015: RET // 戻る
5. リンク
link main.o + fmt.a + runtime.a → 実行ファイル
リンカの処理:
- シンボル解決 (fmt.Println のアドレスを特定)
- アドレス再配置 (CALL 命令のアドレスを確定)
- セクション結合 (.text, .data, .rodata)
- エントリポイント設定 (runtime.main → main.main)
6. 実行ファイルを一時ディレクトリに保存
/var/folders/xx/yy/T/go-build123456789/b001/exe/main
7. 実行
exec("/var/folders/.../main")
8. クリーンアップ (終了後)
rm -rf /var/folders/xx/yy/T/go-build123456789/
🔑 go build の詳細動作:
go build の実行フロー:
1. ビルドキャッシュを確認
cache_dir = go env GOCACHE
→ ~/.cache/go-build/
各ファイルのハッシュを計算:
hash = SHA256(content + compiler_version + build_flags)
if cache[hash] exists:
use cached object file
else:
compile
2. 依存関係の解析
import graph:
main.go
├─ fmt
│ ├─ io
│ ├─ os
│ └─ ...
└─ mypackage
└─ strings
ビルド順序を決定 (トポロジカルソート):
1. io
2. os
3. fmt
4. strings
5. mypackage
6. main
3. 並列コンパイル
parallel for each package:
compile package → object file
CPU コア数に応じて並列化
GOMAXPROCS=8 なら最大8パッケージ同時コンパイル
4. リンク
link all object files
セクション配置:
┌──────────────────────────────────┐
│ ELF/Mach-O/PE ヘッダー │
├──────────────────────────────────┤
│ .text (コード) │
│ ├─ runtime.main │
│ ├─ main.main │
│ ├─ fmt.Println │
│ └─ ... │
├──────────────────────────────────┤
│ .rodata (読み取り専用データ) │
│ ├─ 文字列リテラル │
│ └─ 定数 │
├──────────────────────────────────┤
│ .data (初期化済みグローバル変数) │
├──────────────────────────────────┤
│ .bss (未初期化グローバル変数) │
├──────────────────────────────────┤
│ .gopclntab (関数テーブル) │
│ ← スタックトレース用 │
└──────────────────────────────────┘
5. 実行ファイルを出力
chmod +x myapp
💡 ビルドキャッシュの効果:
初回ビルド:
$ time go build
real 0m5.234s ← 5秒
2回目のビルド (変更なし):
$ time go build
real 0m0.123s ← 0.1秒 (40倍速い!)
変更後のビルド (main.go のみ変更):
$ time go build
real 0m0.534s ← 0.5秒 (変更ファイルのみ再コンパイル)
キャッシュの仕組み:
┌────────────────────────────────────┐
│ ~/.cache/go-build/ │
├────────────────────────────────────┤
│ a1/ │
│ a1b2c3.../ │
│ _pkg_.a ← コンパイル済みパッケージ
│
│ hash計算:
│ content_hash = SHA256(source_code)
│ build_id = SHA256(
│ content_hash +
│ compiler_version +
│ GOOS +
│ GOARCH +
│ build_flags
│ )
│
│ キャッシュヒット判定:
│ if cache[build_id].mtime > source.mtime:
│ use cache
│ else:
│ rebuild
└────────────────────────────────────┘
リリースビルド最適化
# リリースビルド(最適化)
go build -ldflags="-s -w" -o myapp
🔑 -ldflags の詳細:
-ldflags = リンカフラグ (linker flags)
-s: デバッグシンボルを削除
.symtab セクションを削除
┌─ 通常のバイナリ ────────────┐
│ .text (10 MB) │
│ .data (1 MB) │
│ .symtab (5 MB) ← 削除 │
│ 関数名とアドレスの対応表 │
│ 変数名とアドレスの対応表 │
└─────────────────────────────┘
-w: DWARF デバッグ情報を削除
.debug_* セクションを削除
┌─ 通常のバイナリ ────────────┐
│ .debug_info (20 MB) ← 削除 │
│ 型情報、行番号情報 │
│ .debug_line (3 MB) ← 削除 │
│ ソースコード行番号マップ │
└─────────────────────────────┘
サイズ比較:
通常ビルド:
$ go build -o app
$ ls -lh app
-rwxr-xr-x 1 user staff 15M app
リリースビルド:
$ go build -ldflags="-s -w" -o app
$ ls -lh app
-rwxr-xr-x 1 user staff 8.2M app
↑ 約45%縮小
デメリット:
- デバッガが使えない (dlv, gdb)
- panic 時のスタックトレースに関数名が出ない
- プロファイリングが困難
用途:
開発: デバッグ情報ありでビルド
本番: -s -w で軽量化
その他の最適化フラグ:
# バージョン情報を埋め込む
go build -ldflags="-X main.version=1.0.0 -X main.buildTime=$(date +%Y%m%d)"
# 静的リンク (外部ライブラリなし)
CGO_ENABLED=0 go build -ldflags="-s -w"
# 完全な最適化
go build \
-ldflags="-s -w" \
-trimpath \
-buildmode=pie
# 最小サイズビルド
go build -ldflags="-s -w" -gcflags="all=-l -N" -trimpath
💡 各フラグの意味:
-trimpath:
バイナリに埋め込まれるファイルパスを削除
通常:
panic: runtime error
/Users/alice/projects/myapp/main.go:10
↑ 開発者のパス情報が漏洩
-trimpath 使用:
panic: runtime error
myapp/main.go:10
↑ 相対パスのみ
-buildmode=pie:
Position Independent Executable
ASLR (Address Space Layout Randomization) 対応
メモリ配置:
通常:
┌──────────────────────────────┐
│ 0x400000: .text (常に固定) │ ← 攻撃者が予測可能
│ 0x600000: .data │
└──────────────────────────────┘
PIE:
実行ごとにランダムなアドレス
┌──────────────────────────────┐
│ 0x7f8a1234: .text (ランダム) │ ← 予測不可能
│ 0x7f8a5678: .data │
└──────────────────────────────┘
セキュリティ向上、わずかに性能低下
-gcflags="all=-l -N":
コンパイラフラグ
-l: インライン展開を無効化
-N: 最適化を無効化
デバッグビルド用(サイズ削減には不向き)
インストール
# $GOPATH/binにインストール
go install
# 外部ツールのインストール
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
🔑 go install の動作:
go install の実行フロー:
1. ビルド
go build と同様にコンパイル・リンク
2. インストール先を決定
if GOBIN is set:
install_dir = GOBIN
else:
install_dir = GOPATH/bin
通常: ~/go/bin/
3. バイナリをコピー
cp /tmp/go-build.../exe/myapp ~/go/bin/myapp
4. 実行権限を付与
chmod +x ~/go/bin/myapp
5. PATH の確認
if ~/go/bin not in PATH:
warn "Add ~/go/bin to your PATH"
外部ツールのインストール:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
💡 @latest の意味:
バージョン指定の方法:
@latest:
最新のセマンティックバージョンタグ
v1.0.0, v1.1.0, v2.0.0 がある場合 → v2.0.0
@v1.54.2:
特定バージョン
@v1:
v1.x.x の最新
v1.50.0, v1.54.2 がある場合 → v1.54.2
@master:
ブランチを指定(非推奨)
再現性がない
@commit_hash:
特定コミット
v0.0.0-20231110120000-abc123def456
テスト - 内部メカニズム
# すべてのテストを実行
go test
# サブディレクトリも含む
go test ./...
# 詳細出力
go test -v
# カバレッジ測定
go test -cover
# カバレッジレポート生成
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
🔑 go test の完全動作:
go test の実行フロー:
1. テストファイルを検出
find . -name "*_test.go"
例:
math.go ← 本体
math_test.go ← テスト
2. テストパッケージをビルド
package math_test (外部テスト)
または
package math (内部テスト)
3. テストバイナリを生成
compile:
math.go + math_test.go → math.test
バイナリ構造:
┌──────────────────────────────────┐
│ runtime │
│ testing パッケージ │
│ math パッケージ │
│ math_test パッケージ │
│ ┌──────────────────────────────┐ │
│ │ 生成されたmain関数: │ │
│ │ func main() { │ │
│ │ testing.Main( │ │
│ │ tests: [ │ │
│ │ TestAdd, │ │
│ │ TestSubtract, │ │
│ │ ], │ │
│ │ benchmarks: [ │ │
│ │ BenchmarkAdd, │ │
│ │ ], │ │
│ │ ) │ │
│ │ } │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────┘
4. テストバイナリを実行
exec ./math.test
5. 各テストを実行
for each test in tests:
setUp()
result = runTest(test)
tearDown()
if result.failed:
print "FAIL"
else:
print "PASS"
6. 結果を集計
PASS math 0.123s
7. クリーンアップ
rm math.test
カバレッジ測定の仕組み:
go test -cover
💡 カバレッジ計測の内部動作:
1. ソースコードを改変
元のコード:
func Add(a, b int) int {
return a + b
}
計測用に変換:
var GoCover_0_abc123 = struct {
Count [1]uint32
Pos [3]uint32
NumStmt [1]uint16
}{
Pos: [3]uint32{1, 2, 3}, // 行番号
NumStmt: [1]uint16{1}, // 文の数
}
func Add(a, b int) int {
GoCover_0_abc123.Count[0]++ ← カウンタを挿入
return a + b
}
2. テストを実行
各ブロックの実行回数をカウント
3. 実行後に集計
total_blocks = 100
executed_blocks = 85
coverage = 85 / 100 = 85%
4. カバレッジファイルに保存
coverage.out:
mode: set
mypackage/math.go:1.23,2.10 1 1 ← 実行された
mypackage/math.go:3.15,4.20 1 0 ← 実行されなかった
カバレッジHTML生成:
go tool cover -html=coverage.out
HTML生成の仕組み:
1. coverage.out を解析
executed_lines = [1, 2, 5, 6, 7]
not_executed = [3, 4, 8]
2. ソースコードを読み込み
source = readFile("math.go")
3. HTML生成
<style>
.cov0 { color: rgb(192, 0, 0) } ← 未実行 (赤)
.cov1 { color: rgb(0, 0, 0) } ← 実行済み (黒)
.cov8 { color: rgb(0, 128, 0) } ← 複数回実行 (緑)
</style>
<pre>
<span class="cov1">func Add(a, b int) int {</span>
<span class="cov1"> return a + b</span>
<span class="cov1">}</span>
<span class="cov0">func Unused() {</span> ← 赤色表示
<span class="cov0"> // 未実行コード</span>
<span class="cov0">}</span>
</pre>
4. ブラウザで開く
open coverage.html
標準ツールチェーン - 詳細動作
1. go fmt - コードフォーマッタ
Goはコードスタイルが統一されています。
# ファイルをフォーマット
go fmt main.go
# すべてのファイルをフォーマット
go fmt ./...
# gofmtを直接使用(より細かい制御)
gofmt -w main.go
🔑 go fmt の内部動作:
1. ソースコードを読み込み
source = readFile("main.go")
2. パース (構文解析)
ast = parse(source)
AST (抽象構文木):
File
├─ Package: "main"
├─ Imports
│ └─ "fmt"
└─ Decls
└─ FuncDecl
├─ Name: "main"
└─ Body
└─ ExprStmt
└─ CallExpr
└─ "fmt.Println"
3. フォーマットルールを適用
ルール:
- インデント: タブ
- 括弧の位置: K&R スタイル
- 演算子の前後: スペース
- 改行: 一貫性
4. AST から再生成
formatted = print(ast)
アルゴリズム:
func print(node) {
switch node.Type {
case FuncDecl:
write("func ")
write(node.Name)
write("(")
for param in node.Params:
write(param)
write(", ")
write(") ")
if node.ReturnType:
write(node.ReturnType)
write(" ")
write("{")
newline()
indent++
print(node.Body)
indent--
write("}")
}
}
5. ファイルに書き戻す
writeFile("main.go", formatted)
フォーマット例:
// フォーマット前
package main
import "fmt"
func main(){fmt.Println("Hello")}
// フォーマット後
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
💡 なぜタブを使うのか:
Go の哲学:
1. 議論の余地をなくす
スペース2個 vs 4個 → 終わりのない論争
タブ → 設定不要、議論終了
2. アクセシビリティ
視覚障害者がエディタで表示幅を調整可能
タブ幅を8にする人、4にする人、両方対応
3. ファイルサイズ削減
スペース4個 = 4バイト
タブ1個 = 1バイト
2. go vet - 静的解析
コードの潜在的な問題を検出します。
# 静的解析を実行
go vet main.go
# すべてのパッケージを解析
go vet ./...
🔑 go vet の検査項目:
1. Printf フォーマット文字列
fmt.Printf("%d", "string") ← エラー
検査方法:
- Printf の第1引数が文字列リテラルか確認
- フォーマット指定子 (%d, %s, %v) を解析
- 引数の型と照合
AST 解析:
CallExpr {
Fun: "fmt.Printf"
Args: [
BasicLit{Value: "\"%d\""}, ← %d (int期待)
BasicLit{Value: "\"string\""} ← string型
]
}
↓
型不一致を検出
2. 無意味な比較
if x < x { ... } ← 常にfalse
検査:
BinaryExpr {
X: Ident{Name: "x"}
Op: "<"
Y: Ident{Name: "x"}
}
↓
X と Y が同じ → 警告
3. 無限ループ
for i := 0; i < 10; { ← i が増加しない
fmt.Println(i)
}
4. unreachable code
func example() {
return
fmt.Println("Never executed") ← 到達不可能
}
制御フロー解析:
CFG (Control Flow Graph):
┌─────────────┐
│ return │
└─────────────┘
↓ (無条件)
┌─────────────┐
│ Println │ ← 到達不可能
└─────────────┘
5. 未使用の変数
x := 10 ← 使用されていない
y := 20
fmt.Println(y)
6. atomic 操作の誤用
var x int64
atomic.AddInt64(&x, 1)
y := x ← 競合状態!
7. defer の誤用
defer mutex.Unlock() ← OK
defer mutex.Unlock ← NG (関数ポインタのみ)
検出される問題の例:
// 問題あり: Printfのフォーマット指定子が間違っている
fmt.Printf("Value: %d", "string") // vetが警告
// 問題あり: 無意味な比較
if x < x { // vetが警告
// ...
}
3. go test - テストランナー (詳細)
# テスト実行
go test
# ベンチマーク実行
go test -bench=.
# 特定のテストのみ実行
go test -run TestAdd
# 並列実行数を指定
go test -parallel 4
# レースコンディション検出
go test -race
🔑 -race フラグの動作原理:
レースディテクタの仕組み:
1. コードを計装 (instrumentation)
元のコード:
var counter int
counter++
計装後:
var counter int
runtime.racewrite(&counter, sizeof(int)) ← 書き込み記録
counter++
読み取り:
x := counter
↓
runtime.raceread(&counter, sizeof(int)) ← 読み取り記録
x := counter
2. 実行時に監視
各goroutineのメモリアクセスを記録
メモリアドレス 0x1234 への アクセス履歴:
┌────────────────────────────────────┐
│ Goroutine 1: Write at 10:23:45.123 │
│ Goroutine 2: Write at 10:23:45.125 │ ← 同時書き込み!
└────────────────────────────────────┘
3. データ競合を検出
条件:
- 2つ以上の goroutine が
- 同じメモリアドレスに
- 少なくとも1つが書き込みで
- 同期なしでアクセス
4. レポート出力
WARNING: DATA RACE
Write at 0x00c000010088 by goroutine 7:
main.increment()
/path/to/main.go:15 +0x3e
Previous write at 0x00c000010088 by goroutine 6:
main.increment()
/path/to/main.go:15 +0x3e
テストファイルの例 (math_test.go):
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
💡 ベンチマークの実行:
go test -bench=.
ベンチマークの仕組み:
1. 最適なイテレーション数を決定
初期値: b.N = 1
測定:
start = time.Now()
for i := 0; i < b.N; i++ {
Add(2, 3)
}
duration = time.Since(start)
if duration < 1秒:
b.N *= 2 // 倍増
再測定
else:
十分なデータ
2. 統計情報を計算
total_time = 1.234s
iterations = 1000000
ns_per_op = total_time / iterations = 1234 ns/op
3. 出力
BenchmarkAdd-8 1000000 1234 ns/op
↑ ↑ ↑
名前 並列数 1回あたりの時間
4. go build - ビルダー (詳細)
# 現在のプラットフォーム向けにビルド
go build
# クロスコンパイル
GOOS=linux GOARCH=amd64 go build
# ビルドタグを使用
go build -tags production
# リンカフラグでバイナリサイズ削減
go build -ldflags="-s -w"
# 静的リンク
CGO_ENABLED=0 go build
# ビルド情報を埋め込む
go build -ldflags="-X main.version=1.0.0"
🔑 ビルドタグの仕組み:
// +build linux
package platform
func getPlatform() string {
return "Linux"
}
// +build darwin
package platform
func getPlatform() string {
return "macOS"
}
ビルド時:
GOOS=linux go build
→ linux タグのファイルのみコンパイル
GOOS=darwin go build
→ darwin タグのファイルのみコンパイル
複雑な条件:
// +build linux,amd64
→ linux AND amd64
// +build linux darwin
→ linux OR darwin
// +build !windows
→ NOT windows
プロジェクト構造のベストプラクティス - 設計思想
小規模プロジェクト
myapp/
├── go.mod
├── go.sum
├── main.go
├── handler.go
└── handler_test.go
中規模プロジェクト
myapp/
├── go.mod
├── go.sum
├── main.go
├── cmd/
│ ├── server/
│ │ └── main.go
│ └── client/
│ └── main.go
├── internal/
│ ├── handler/
│ │ ├── handler.go
│ │ └── handler_test.go
│ └── database/
│ └── db.go
└── pkg/
└── utils/
└── utils.go
🔑 internal/ の特別な意味:
internal/ ディレクトリの仕組み:
コンパイラによる強制:
myapp/
├── internal/
│ └── secret/
│ └── api.go
└── cmd/
└── main.go
myapp/cmd/main.go:
import "myapp/internal/secret" ← OK
other-project/main.go:
import "myapp/internal/secret" ← コンパイルエラー!
error: use of internal package myapp/internal/secret not allowed
ルール:
internal/ とその親ディレクトリからのみアクセス可能
階層例:
a/
├── internal/
│ └── b/
│ └── c.go
├── d/
│ └── e.go
└── f/
└── g.go
アクセス可能:
a/d/e.go → a/internal/b ○
a/f/g.go → a/internal/b ○
アクセス不可:
other-project → a/internal/b ×
実装:
コンパイラがimport pathをチェック:
if strings.Contains(import_path, "/internal/"):
caller_module = get_module_root(caller)
internal_module = extract_before_internal(import_path)
if caller_module != internal_module:
error("cannot import internal package")
大規模プロジェクト (クリーンアーキテクチャ)
myapp/
├── go.mod
├── go.sum
├── cmd/
│ ├── api/
│ │ └── main.go
│ └── worker/
│ └── main.go
├── internal/
│ ├── domain/ ← ビジネスロジック(依存なし)
│ │ ├── user/
│ │ │ ├── user.go
│ │ │ └── repository.go ← インターフェース
│ │ └── product/
│ ├── usecase/ ← ユースケース(domainに依存)
│ │ ├── user_usecase.go
│ │ └── product_usecase.go
│ ├── repository/ ← データアクセス(domainのinterfaceを実装)
│ │ ├── user_repository.go
│ │ └── product_repository.go
│ └── infrastructure/ ← 外部依存
│ ├── database/
│ │ └── postgres.go
│ ├── cache/
│ │ └── redis.go
│ └── http/
│ └── server.go
├── pkg/ ← 外部公開可能なライブラリ
│ ├── logger/
│ └── config/
├── api/
│ └── openapi.yaml
├── scripts/
├── configs/
└── docs/
💡 依存方向の原則:
依存グラフ(Clean Architecture):
┌─────────────────────────────────────┐
│ cmd/ │
│ (エントリポイント) │
└──────────┬──────────────────────────┘
│ depends on
↓
┌─────────────────────────────────────┐
│ infrastructure/ │
│ (HTTP, DB, Cache) │
└──────────┬──────────────────────────┘
│ depends on
↓
┌─────────────────────────────────────┐
│ repository/ │
│ (データアクセス実装) │
└──────────┬──────────────────────────┘
│ implements
↓
┌─────────────────────────────────────┐
│ usecase/ │
│ (ビジネスロジック) │
└──────────┬──────────────────────────┘
│ depends on
↓
┌─────────────────────────────────────┐
│ domain/ │
│ (エンティティ、インターフェース) │
│ ← 依存されるのみ、他に依存しない │
└─────────────────────────────────────┘
ルール:
1. 内側の層は外側を知らない
2. インターフェースで依存を逆転
3. domain/ が最も安定
ディレクトリの役割:
| ディレクトリ | 説明 | 依存 |
|---|---|---|
| `cmd/` | 実行ファイルのエントリーポイント | 全てに依存可能 |
| `internal/` | 外部に公開しないパッケージ | プロジェクト内のみ |
| `pkg/` | 外部に公開可能なライブラリ | 最小限の依存 |
| `api/` | API定義(OpenAPI、gRPCなど) | スキーマのみ |
| `configs/` | 設定ファイル | なし |
| `scripts/` | ビルド、デプロイスクリプト | なし |
| `docs/` | ドキュメント | なし |
エディタとIDEのセットアップ - 深層設定
Visual Studio Code
推奨拡張機能:
{
"recommendations": [
"golang.go",
"streetsidesoftware.code-spell-checker"
]
}
settings.json:
{
"go.useLanguageServer": true,
"go.formatTool": "goimports",
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
"editor.formatOnSave": true
}
🔑 gopls (Go Language Server) の動作:
Language Server Protocol (LSP):
┌─────────────────┐ ┌─────────────────┐
│ VS Code │ ←──LSP──→ │ gopls │
│ (クライアント) │ │ (サーバー) │
└─────────────────┘ └─────────────────┘
↑ ↑
│ │
ユーザー操作 コード解析
- 入力 - パース
- 補完要求 - 型チェック
- 定義ジャンプ - シンボル解決
通信例:
1. ファイル開く
Client → Server:
{
"method": "textDocument/didOpen",
"params": {
"uri": "file:///path/to/main.go",
"text": "package main\n..."
}
}
2. 補完要求
Client → Server:
{
"method": "textDocument/completion",
"params": {
"uri": "file:///path/to/main.go",
"position": {"line": 5, "character": 10}
}
}
Server → Client:
{
"result": {
"items": [
{"label": "Println", "kind": "Function"},
{"label": "Printf", "kind": "Function"}
]
}
}
3. 定義ジャンプ
fmt.Println にカーソル
Server:
- AST を検索
- シンボルテーブルで fmt.Println を検索
- 定義場所を特定: $GOROOT/src/fmt/print.go:274
Client:
- ファイルを開く
- 274行目にジャンプ
デバッグツール - 深層メカニズム
Delve(公式デバッガ)
# インストール
go install github.com/go-delve/delve/cmd/dlv@latest
# デバッグモードで実行
dlv debug main.go
# ブレークポイントを設定
(dlv) break main.main
(dlv) continue
# 変数の値を表示
(dlv) print myVar
# スタックトレース
(dlv) stack
🔑 Delve の動作原理:
デバッガの仕組み:
1. プロセスのアタッチ
ptrace(PTRACE_ATTACH, pid) ← Linuxシステムコール
2. ブレークポイントの設定
元のコード:
0x1234: MOVQ $0x10, %rax
0x123c: ADDQ $0x5, %rax
ブレークポイント設定:
original_instruction = read_memory(0x1234) // MOVQ
write_memory(0x1234, 0xCC) // INT 3 (ブレークポイント命令)
変更後:
0x1234: INT 3 ← トラップ
0x123c: ADDQ $0x5, %rax
3. 実行
プログラムが 0x1234 に到達
→ INT 3 を実行
→ CPU が例外を発生
→ OS がデバッガに通知
4. デバッガが制御を取得
- 元の命令を復元
- レジスタ、メモリを読み取り
- ユーザーにプロンプト表示
5. 変数の表示
(dlv) print myVar
内部動作:
- シンボルテーブルで myVar のアドレスを検索
- DWARF デバッグ情報で型を確認
- メモリからデータ読み取り
- 型に応じてフォーマット
例:
myVar:
Address: 0xc000010088
Type: int
Size: 8 bytes
Value: read_memory(0xc000010088, 8) = 0x000000000000002A
= 42 (10進数)
レースコンディション検出 (詳細)
# データ競合を検出
go run -race main.go
go test -race ./...
検出例:
package main
import (
"fmt"
"sync"
)
func main() {
counter := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // データ競合!
}()
}
wg.Wait()
fmt.Println(counter)
}
実行すると警告が表示されます:
WARNING: DATA RACE
Write at 0x... by goroutine 7:
main.main.func1()
/path/to/main.go:14 +0x3e
Previous write at 0x... by goroutine 6:
main.main.func1()
/path/to/main.go:14 +0x3e
💡 ThreadSanitizer の仕組み:
Go の race detector は ThreadSanitizer を使用:
1. メモリアクセスの記録
各 goroutine に shadow state を割り当て
Goroutine 1:
┌──────────────────────────────────┐
│ Shadow Memory │
├──────────────────────────────────┤
│ 0x1234: {tid:1, clock:10, write} │
│ 0x5678: {tid:1, clock:11, read} │
└──────────────────────────────────┘
Goroutine 2:
┌──────────────────────────────────┐
│ Shadow Memory │
├──────────────────────────────────┤
│ 0x1234: {tid:2, clock:15, write} │ ← 同じアドレス!
└──────────────────────────────────┘
2. ベクタークロック
各 goroutine の論理時刻を追跡
Goroutine 1: [10, 0, 0]
Goroutine 2: [5, 15, 0]
Happens-before 関係:
もし G1[i] < G2[i] for all i:
G1 happens before G2 (競合なし)
そうでなければ:
concurrent access (競合あり)
3. 検出
counter++ を実行:
Goroutine 7:
- Read counter (0x1234)
- Increment
- Write counter (0x1234)
Shadow memory チェック:
previous_access = shadow[0x1234]
if previous_access.tid != current.tid:
if !happens_before(previous, current):
RACE DETECTED!
パフォーマンスプロファイリング - 完全ガイド
CPUプロファイル
import (
"os"
"runtime/pprof"
)
func main() {
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// プロファイリング対象のコード
heavyComputation()
}
# プロファイル解析
go tool pprof cpu.prof
# Webインターフェースで表示
go tool pprof -http=:8080 cpu.prof
🔑 CPU プロファイラの仕組み:
サンプリングベースのプロファイリング:
1. タイマーを設定
setitimer(ITIMER_PROF, 10ms)
↑ 10ミリ秒ごとに SIGPROF シグナルを送信
2. シグナルハンドラ
signal_handler(SIGPROF):
current_pc = get_program_counter()
current_sp = get_stack_pointer()
stack_trace = unwind_stack(current_pc, current_sp)
record(stack_trace)
3. スタックアンワインド
現在のPC: 0x1234 (function A)
スタックフレーム:
┌──────────────────────┐ ← SP
│ Return Address: 0x56 │ → function B
├──────────────────────┤
│ Return Address: 0x78 │ → function C
├──────────────────────┤
│ Return Address: 0x9a │ → main
└──────────────────────┘
記録:
main → C → B → A (100回)
main → C → B (50回)
main → D (25回)
4. 集計
A: 100サンプル (40%)
B: 150サンプル (60%)
C: 150サンプル (60%)
D: 25サンプル (10%)
main: 250サンプル (100%)
5. 可視化
フレームグラフ:
┌───────────────────────────────────┐
│ main (100%) │
├─────────────┬─────────────────────┤
│ C (60%) │ D (10%) │
├─────────────┤ │
│ B (60%) │ │
├─────────────┤ │
│ A (40%) │ │
└─────────────┴─────────────────────┘
メモリプロファイル
import (
"os"
"runtime/pprof"
)
func main() {
heavyComputation()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
}
🔑 メモリプロファイラの仕組み:
アロケーション追跡:
1. メモリ割り当てをフック
mallocgc(size, typ, flags) {
ptr = allocate_memory(size)
if profiling_enabled {
stack_trace = get_stack_trace()
record_allocation(ptr, size, stack_trace)
}
return ptr
}
2. 記録
Allocation記録:
┌─────────────────────────────────────┐
│ Address: 0xc000100000 │
│ Size: 1024 bytes │
│ Stack: │
│ main.makeSlice:10 │
│ main.process:20 │
│ main.main:5 │
├─────────────────────────────────────┤
│ Address: 0xc000100400 │
│ Size: 2048 bytes │
│ Stack: │
│ main.makeMap:15 │
│ main.main:6 │
└─────────────────────────────────────┘
3. 集計
Stack trace別の合計:
main.makeSlice → 10MB (1000回)
main.makeMap → 5MB (500回)
4. プロファイル出力
# main.makeSlice main.go:10
# 10485760 bytes (1000 allocations)
# main.makeMap main.go:15
# 5242880 bytes (500 allocations)
まとめ
この章では、Go開発環境の構築とツールチェーンについて、機械レベルまで深く学びました。
重要ポイント:
- インストール: 自己完結したツールチェーン、プラットフォーム別の最適化
- 環境変数: GOPATH、GOROOT、GOOS/GOARCHの内部動作
- Go Modules: 依存解決、バージョン管理、チェックサムによる検証
- goコマンド: コンパイル、リンク、キャッシュの仕組み
- プロジェクト構造: internal/の特殊性、依存方向の原則
- デバッグツール: Delve、レースディテクター、プロファイラの動作原理
機械レベルの理解:
- Goコンパイラは AST → SSA → 機械語 の変換を行う
- リンカはシンボル解決とアドレス再配置を実行
- レースディテクタは ThreadSanitizer で並行アクセスを検出
- プロファイラはサンプリングで性能ボトルネックを特定
- GOPATH と GOROOT の違いを説明してください。それぞれがコンパイル時にどのように使用されますか?
- go mod tidy コマンドは何をするか、内部動作を含めて説明してください。
- go build と go install の違いは何ですか?生成されるバイナリの配置先は?
- internal/ ディレクトリの特別な意味と、コンパイラによる強制の仕組みを説明してください。
- go run main.go を実行したとき、裏で何が起きているか、一時ファイルの生成からクリーンアップまで説明してください。
- クロスコンパイル時に GOOS=linux GOARCH=amd64 を指定すると、生成される機械語にどのような違いが出ますか?
- go get でパッケージをダウンロードする際、Go プロキシ (proxy.golang.org) を経由する理由は何ですか?
- ビルドキャッシュはどのように動作し、どのような場合にキャッシュがヒットしますか?ハッシュ計算の仕組みを説明してください。
- go vet はどのようにして Printf のフォーマット文字列の誤りを検出しますか?
- gopls (Language Server) がコード補完を提供する仕組みを、LSPプロトコルレベルで説明してください。
- go test -race でレースコンディションを検出する仕組みを、ThreadSanitizerとベクタークロックの観点から説明してください。
- Delve デバッガがブレークポイントを設定する際、機械語レベルで何が起きていますか? INT 3 命令の役割は?
- CPU プロファイラのサンプリング方式と、スタックアンワインドの仕組みを説明してください。
- -ldflags="-s -w" でバイナリサイズが縮小される理由を、ELF/Mach-Oセクション構造の観点から説明してください。
- Go Modules の go.sum ファイルが改ざん検出にどのように使われるか、チェックサム検証のフローを説明してください。
自己チェック問題
基本問題
中級問題
上級問題
---
次の章では、Goの基本構文を学び、実際にコードを書いていきます。