第6章: 関数とメソッド
学習目標
この章を終えると、以下ができるようになります:
- 関数を定義し、複数の値を返せる
- 名前付き戻り値と裸のreturnを使える
- メソッドとレシーバを理解できる
- 関数型プログラミングの基本を使える
- 関数呼び出しのスタック動作を理解できる
- クロージャとdeferの内部実装を理解できる
1. 関数の基本
1.1 関数定義
// 基本的な関数
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
// 戻り値を持つ関数
func add(a, b int) int {
return a + b
}
// 同じ型の引数は省略可能
func multiply(a, b, c int) int {
return a * b * c
}
// 異なる型の引数
func describe(name string, age int, active bool) {
fmt.Printf("%s is %d years old, active: %t\n", name, age, active)
}
1.2 複数戻り値
Goの最も特徴的な機能の一つです。エラーハンドリングでよく使われます。
// 複数の値を返す
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// 使用例
func main() {
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println("Result:", result)
}
// 3つ以上の値も可能
func getUserInfo(id int) (string, int, string, error) {
// データベースから取得
return "Alice", 25, "alice@example.com", nil
}
戻り値の無視:
// 不要な戻り値は _ で無視
result, _ := divide(10, 2) // エラーを無視
// すべて無視
_, _, _, _ = getUserInfo(1)
1.3 名前付き戻り値
戻り値に名前を付けることができます。
// 名前付き戻り値
func calculate(x, y int) (sum, product int) {
sum = x + y
product = x * y
return // 裸のreturn(名前付き戻り値を返す)
}
// 明示的に返すこともできる
func calculateExplicit(x, y int) (sum, product int) {
sum = x + y
product = x * y
return sum, product // 明示的return
}
実用例: エラーハンドリング:
func readFile(filename string) (content []byte, err error) {
file, err := os.Open(filename)
if err != nil {
return // nil, errを返す
}
defer file.Close()
content, err = io.ReadAll(file)
if err != nil {
return // nil, errを返す
}
return // content, nilを返す
}
💡 注意点:
// 裸のreturnは短い関数でのみ使う
func bad() (result int, err error) {
// ... 100行のコード ...
if something {
result = 42
} else if other {
result = 99
}
// ... さらに100行 ...
return // resultの値がわかりにくい!
}
// 長い関数では明示的returnを使う
func better() (int, error) {
// ... コード ...
return 42, nil // 明確
}
1.4 可変長引数
// 可変長引数
func sum(numbers ...int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
// 使用例
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
fmt.Println(sum()) // 0
// スライスを展開して渡す
nums := []int{1, 2, 3, 4}
fmt.Println(sum(nums...)) // 10
通常引数と組み合わせ:
// 可変長引数は最後のパラメータのみ
func format(prefix string, values ...interface{}) string {
var result string
result += prefix + ": "
for i, v := range values {
if i > 0 {
result += ", "
}
result += fmt.Sprintf("%v", v)
}
return result
}
fmt.Println(format("Values", 1, 2, 3)) // Values: 1, 2, 3
fmt.Println(format("Mixed", "a", 42, true)) // Mixed: a, 42, true
2. 関数呼び出しのスタック動作
2.1 CALL/RET命令の仕組み
🔑 重要: 関数呼び出しは、CPUのCALL命令とスタック操作によって実現されます。
CALL命令の動作
CALL命令が実行されるとき:
1. 現在のRIP(命令ポインタ)の次の命令アドレスをスタックにPUSH
2. RIPに関数の先頭アドレスをセット
3. 関数本体へジャンプ
メモリレイアウト:
高位アドレス
┌──────────────┐
│ 引数3 │
├──────────────┤
│ 引数2 │
├──────────────┤
│ 引数1 │
├──────────────┤
│ 戻りアドレス │ ← CALL命令がPUSH
├──────────────┤
│ ローカル1 │
├──────────────┤
│ ローカル2 │
├──────────────┤ ← SP (Stack Pointer)
低位アドレス
RET命令の動作
RET命令が実行されるとき:
1. スタックから戻りアドレスをPOP
2. RIPにその戻りアドレスをセット
3. 呼び出し元の次の命令へジャンプ
具体例:
func main() {
x := add(3, 5) // ①CALL命令
println(x) // ④ここに戻る
}
func add(a, b int) int {
sum := a + b // ②実行
return sum // ③RET命令
}
実行フロー:
main関数内:
mov rdi, 3 ; 第1引数
mov rsi, 5 ; 第2引数
call add ; ①戻りアドレスをPUSH & ジャンプ
add関数内:
; プロローグ
push rbp ; ②ベースポインタを保存
mov rbp, rsp ; 新しいフレーム設定
; 関数本体
mov rax, rdi ; a を rax へ
add rax, rsi ; rax += b
; エピローグ
pop rbp ; ③ベースポインタを復元
ret ; 戻りアドレスをPOP & ジャンプ
main関数へ戻る:
mov QWORD [x], rax ; ④戻り値を保存
2.2 スタックフレーム構造
🔑 スタックフレーム: 各関数呼び出しごとに作られるメモリ領域
完全なスタックフレーム構造:
高位アドレス
┌─────────────────┐
│ 呼び出し元の │
│ スタックフレーム │
├─────────────────┤
│ 引数 N │
│ ... │
│ 引数 2 │
│ 引数 1 │ ← 引数エリア
├─────────────────┤
│ 戻りアドレス │ ← CALL がPUSH
├─────────────────┤
│ 保存された RBP │ ← 関数プロローグがPUSH
├─────────────────┤ ← RBP (Base Pointer)
│ ローカル変数1 │
│ ローカル変数2 │
│ ... │
│ ローカル変数N │ ← ローカル変数エリア
├─────────────────┤
│ 一時変数 │ ← 一時領域
├─────────────────┤ ← RSP (Stack Pointer)
│ 次の関数の引数 │
低位アドレス
アクセス方法:
引数アクセス: [RBP + 16], [RBP + 24], ...
ローカル変数: [RBP - 8], [RBP - 16], ...
実例: 複雑な関数呼び出し
func calculate(a, b, c int) (sum, product int) {
sum = a + b + c
product = a * b * c
temp := sum + product // ローカル変数
return sum, product
}
func main() {
s, p := calculate(2, 3, 4)
fmt.Println(s, p)
}
スタックフレームの変化:
main開始:
┌──────────────┐
│ main frame │ ← RSP, RBP
└──────────────┘
calculate呼び出し前(引数セット):
┌──────────────┐
│ main frame │ ← RBP
├──────────────┤
│ a = 2 │
│ b = 3 │
│ c = 4 │ ← 引数をレジスタかスタックへ
└──────────────┘ ← RSP
CALL calculate後:
┌──────────────┐
│ main frame │
├──────────────┤
│ a = 2 │
│ b = 3 │
│ c = 4 │
├──────────────┤
│ 戻りアドレス │
├──────────────┤
│ 保存RBP │ ← 新しいRBP
├──────────────┤
│ sum = 0 │
│ product = 0 │
│ temp = 0 │ ← ローカル変数
└──────────────┘ ← RSP
RET前(戻り値セット):
sum = 9, product = 24 を戻り値レジスタへ
RET後:
┌──────────────┐
│ main frame │ ← RSP, RBP
├──────────────┤
│ s = 9 │
│ p = 24 │
└──────────────┘
2.3 引数と戻り値の配置
従来のスタック渡し(古いGo)
引数はすべてスタック上:
func example(a, b, c int, s string) int
スタック:
高位アドレス
┌──────────────┐
│ s (string) │
│ - ptr │
│ - len │
├──────────────┤
│ c (int) │
├──────────────┤
│ b (int) │
├──────────────┤
│ a (int) │
├──────────────┤
│ 戻りアドレス │
低位アドレス
Go 1.17以降のレジスタ渡し
🔑 重要: Go 1.17からレジスタベースの呼び出し規約に変更されました。
引数の優先順位:
1. 整数・ポインタ: RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11
2. 浮動小数点: XMM0-XMM14
3. レジスタに収まらない場合のみスタック
戻り値の優先順位:
1. 第1戻り値: RAX
2. 第2戻り値: RBX
3. 第3戻り値: RCX
4. それ以降: スタック
例: 2つの整数を引数に取る関数
func add(a, b int) int {
return a + b
}
; Go 1.17以降
add:
mov rax, rdi ; a はRDIレジスタから
add rax, rsi ; b はRSIレジスタから
ret ; 結果はRAXレジスタへ
; Go 1.16以前
add:
mov rax, [rsp+8] ; a はスタックから
mov rbx, [rsp+16] ; b はスタックから
add rax, rbx
mov [rsp+24], rax ; 結果をスタックへ
ret
例: 複数戻り値
func divmod(a, b int) (quotient, remainder int) {
return a / b, a % b
}
divmod:
mov rax, rdi ; a を RAX へ
xor rdx, rdx ; RDX をクリア
div rsi ; RAX / RSI → RAX(商), RDX(余)
mov rbx, rdx ; 余りを RBX へ
ret ; RAX=商, RBX=余り
💡 パフォーマンス改善:
レジスタ渡しの利点:
1. メモリアクセスが不要(L1キャッシュより速い)
2. スタック操作のオーバーヘッド削減
3. 小さな関数で20-30%の高速化
2.4 ネストした関数呼び出し
func main() {
result := funcA(10)
println(result)
}
func funcA(x int) int {
return funcB(x) + 5
}
func funcB(x int) int {
return funcC(x) * 2
}
func funcC(x int) int {
return x + 3
}
スタックの成長過程:
[1] main開始
┌──────────────┐
│ main frame │
└──────────────┘
[2] funcA呼び出し
┌──────────────┐
│ main frame │
├──────────────┤
│ funcA frame │
│ x = 10 │
└──────────────┘
[3] funcB呼び出し
┌──────────────┐
│ main frame │
├──────────────┤
│ funcA frame │
├──────────────┤
│ funcB frame │
│ x = 10 │
└──────────────┘
[4] funcC呼び出し(最深部)
┌──────────────┐
│ main frame │
├──────────────┤
│ funcA frame │
├──────────────┤
│ funcB frame │
├──────────────┤
│ funcC frame │
│ x = 10 │
└──────────────┘
[5] funcC完了 (戻り値: 13)
┌──────────────┐
│ main frame │
├──────────────┤
│ funcA frame │
├──────────────┤
│ funcB frame │
│ RAX = 13 │
└──────────────┘
[6] funcB完了 (戻り値: 26)
┌──────────────┐
│ main frame │
├──────────────┤
│ funcA frame │
│ RAX = 26 │
└──────────────┘
[7] funcA完了 (戻り値: 31)
┌──────────────┐
│ main frame │
│ RAX = 31 │
└──────────────┘
⚠️ スタックオーバーフロー:
// 無限再帰
func infinite(n int) {
println(n)
infinite(n + 1) // スタックが枯渇!
}
// Goのスタックサイズ: 2KB(初期)→ 1GB(最大)
// スタック拡張で数千回の再帰は可能
// しかし無限再帰は最終的にクラッシュ
3. Go ABIと呼び出し規約
3.1 Go ABIの特徴
🔑 ABI (Application Binary Interface): バイナリレベルでの関数呼び出しルール
Go 1.17以降の主な特徴:
1. レジスタベース
- 引数: 整数9個、浮動小数点15個までレジスタ
- 戻り値: 最初の数個はレジスタ
2. スタック拡張
- 動的にスタックを拡張
- 初期2KB → 必要に応じて拡張
3. ゼロレジスタなし
- x86-64にゼロレジスタはないが、XOR で実現
4. 呼び出し先保存(Callee-saved)
- RBX, RBP, R12-R15は関数が保存
- その他は呼び出し元保存
5. GCのためのメタデータ
- スタックマップ
- ポインタビットマップ
3.2 レジスタ使用規則
レジスタ割り当て(x86-64):
整数/ポインタ引数:
RAX: 第1引数 または 第1戻り値
RBX: 第2引数 または 第2戻り値 (callee-saved)
RCX: 第3引数 または 第3戻り値
RDI: 第4引数
RSI: 第5引数
R8: 第6引数
R9: 第7引数
R10: 第8引数
R11: 第9引数
浮動小数点引数:
XMM0-XMM14: 浮動小数点引数・戻り値
特殊用途:
RSP: スタックポインタ
RBP: ベースポインタ(フレームポインタ)
R12-R15: callee-saved(関数内で自由に使える)
R14: 現在のGoroutine情報へのポインタ
一時レジスタ(caller-saved):
RAX, RCX, RDX, RSI, RDI, R8-R11
関数呼び出し時に破壊される可能性
実例:
func complex(a, b, c, d, e, f, g, h, i int) (int, int, int) {
return a+b, c+d, e+f+g+h+i
}
complex:
; 引数レジスタ割り当て:
; RAX=a, RBX=b, RCX=c, RDI=d, RSI=e
; R8=f, R9=g, R10=h, R11=i
; 第1戻り値: a + b
add rax, rbx ; RAX = a + b
; 第2戻り値: c + d
add rcx, rdi ; RCX = c + d
mov rbx, rcx ; RBX = 第2戻り値
; 第3戻り値: e + f + g + h + i
add rsi, r8
add rsi, r9
add rsi, r10
add rsi, r11
mov rcx, rsi ; RCX = 第3戻り値
ret ; RAX, RBX, RCX が戻り値
3.3 スタック拡張メカニズム
💡 Goの特徴: スタックは必要に応じて自動拡張されます。
スタック拡張の仕組み:
[1] 各関数のプロローグでスタックチェック
┌──────────────────┐
│ func example() { │
│ // チェック: │
│ if RSP - size │
│ < stackguard│
│ then 拡張 │
│ } │
└──────────────────┘
[2] 拡張が必要な場合
- 新しい大きなスタック領域を割り当て
- 既存データをコピー
- 古いスタックを破棄
- スタックポインタを更新
スタックサイズ:
初期: 2KB
拡張: 2倍ずつ増加(4KB → 8KB → 16KB ...)
最大: 1GB(64bit)、250MB(32bit)
実装例(疑似コード):
example:
; プロローグ
lea r12, [rsp - 128] ; 必要なスタックサイズ
cmp r12, [r14 + stackguard] ; R14 = Goroutine構造体
jb morestack ; 足りなければ拡張
morestack:
call runtime.morestack_noctxt
jmp example ; 拡張後に再実行
⚠️ 注意点:
// スタック拡張のコスト
func recursive(n int) {
if n == 0 {
return
}
var buffer [1000]byte // 1KBのローカル変数
recursive(n - 1)
}
// 2-3回の再帰でスタック拡張が発生
// 各拡張で:
// 1. 新領域の割り当て: メモリアロケーション
// 2. データコピー: O(stack_size)
// 3. GCのスタックマップ更新
3.4 C言語との比較
Go ABI vs C ABI (System V x86-64):
引数渡し:
C: RDI, RSI, RDX, RCX, R8, R9, 以降スタック
Go: RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11
戻り値:
C: RAXのみ(1個)
Go: RAX, RBX, RCX, ...(複数)
Callee-saved:
C: RBX, RBP, R12-R15
Go: RBX, RBP, R12-R15(同じ)
スタック:
C: 固定サイズ
Go: 動的拡張
特殊:
C: なし
Go: R14 = Goroutine構造体ポインタ
Cgoでの相互運用:
/*
#include <stdio.h>
int c_add(int a, int b) {
return a + b;
}
*/
import "C"
func main() {
// Go → C: ABI変換が必要
result := C.c_add(3, 5)
println(result)
}
Cgo呼び出しのオーバーヘッド:
Go関数呼び出し:
- レジスタセット: 1-2 CPU命令
- CALL/RET: 2 CPU命令
→ 合計 3-4 CPU命令
Cgo呼び出し:
- Goランタイムの退出処理
- Go ABI → C ABI変換
- Cスタックへの切り替え
- C関数実行
- Goスタックへの復帰
- C ABI → Go ABI変換
- Goランタイムの再入処理
→ 合計 数百 CPU命令
パフォーマンス:
純Go: 1ns
Cgo: 100-200ns(100-200倍遅い)
4. 高階関数とクロージャ
4.1 関数を引数として受け取る
// 関数型
type Operation func(int, int) int
// 高階関数
func apply(a, b int, op Operation) int {
return op(a, b)
}
// 使用例
func main() {
add := func(x, y int) int { return x + y }
multiply := func(x, y int) int { return x * y }
fmt.Println(apply(3, 4, add)) // 7
fmt.Println(apply(3, 4, multiply)) // 12
}
実用例: フィルタリング:
// Predicate型
type Predicate func(int) bool
// Filter関数
func Filter(numbers []int, predicate Predicate) []int {
result := make([]int, 0)
for _, n := range numbers {
if predicate(n) {
result = append(result, n)
}
}
return result
}
// 使用例
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 偶数のみ
evens := Filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2, 4, 6, 8, 10]
// 5より大きい
greaterThan5 := Filter(numbers, func(n int) bool {
return n > 5
})
fmt.Println(greaterThan5) // [6, 7, 8, 9, 10]
4.2 関数を返す関数
// 関数を返す
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
// 使用例
add5 := makeAdder(5)
add10 := makeAdder(10)
fmt.Println(add5(3)) // 8
fmt.Println(add10(3)) // 13
実用例: ミドルウェアパターン:
type Handler func(string) string
// ミドルウェア: ロギング
func withLogging(h Handler) Handler {
return func(input string) string {
log.Printf("Input: %s", input)
result := h(input)
log.Printf("Output: %s", result)
return result
}
}
// ミドルウェア: 時間計測
func withTiming(h Handler) Handler {
return func(input string) string {
start := time.Now()
result := h(input)
log.Printf("Took: %v", time.Since(start))
return result
}
}
// ベースハンドラ
func process(input string) string {
return strings.ToUpper(input)
}
// 使用例
func main() {
// ミドルウェアを重ねる
handler := withLogging(withTiming(process))
result := handler("hello")
fmt.Println(result) // HELLO
}
5. クロージャの実装
5.1 クロージャの基本
外側のスコープの変数にアクセスできる関数です。
// カウンターのクロージャ
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
// 使用例
counter1 := makeCounter()
counter2 := makeCounter()
fmt.Println(counter1()) // 1
fmt.Println(counter1()) // 2
fmt.Println(counter2()) // 1(別のカウンター)
fmt.Println(counter1()) // 3
5.2 キャプチャ変数のメモリ配置
🔑 重要: クロージャがキャプチャする変数は、ヒープに「エスケープ」します。
func makeCounter() func() int {
count := 0 // この変数はヒープへエスケープ
return func() int {
count++
return count
}
}
メモリレイアウト:
makeCounter実行前:
スタック:
┌────────────────┐
│ makeCounter │
└────────────────┘
ヒープ: (空)
makeCounter実行中(count定義後):
スタック:
┌────────────────┐
│ makeCounter │
│ count = 0 │ ← 通常はここに配置されるが...
└────────────────┘
エスケープ分析後(コンパイラが検出):
- count は返されるクロージャから参照される
- makeCounter終了後もアクセスされる
- → ヒープに移動!
実際のメモリ配置:
スタック:
┌────────────────┐
│ makeCounter │
│ countPtr ───┐ │
└──────────────┼─┘
│
ヒープ: │
┌──────────────▼─┐
│ count = 0 │ ← ヒープ上に確保
└────────────────┘
makeCounter終了後:
スタック:
┌────────────────┐
│ main │
│ counter1 ───┐ │
└──────────────┼─┘
│
ヒープ: │
┌──────────────▼─┐
│ クロージャ構造 │
│ funcPtr: ... │
│ count: 0 ───┐ │
└──────────────┼─┘
│
┌──────────────▼─┐
│ count = 0 │
└────────────────┘
5.3 関数値の内部構造
💡 関数値: Goでは関数は「ファーストクラス」の値です。
関数値の構造体(runtime.funcval):
type funcval struct {
fn uintptr // 関数本体のアドレス
// 以下、キャプチャした変数
}
通常の関数:
┌──────────────┐
│ funcval │
│ fn: 0x... │ → 関数コード
└──────────────┘
クロージャ:
┌──────────────┐
│ funcval │
│ fn: 0x... │ → クロージャラッパー関数
│ var1: ... │ → キャプチャ変数1
│ var2: ... │ → キャプチャ変数2
│ ... │
└──────────────┘
実装詳細:
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
add5 := makeAdder(5)
コンパイラが生成するコード(疑似コード):
// makeAdder の実装
func makeAdder(x int) func(int) int {
// クロージャ構造体をヒープに割り当て
closure := new(struct {
fn uintptr
x int
})
closure.fn = addrof(closure_wrapper)
closure.x = x
return (*funcval)(closure)
}
// クロージャのラッパー関数
func closure_wrapper(closure *struct{fn uintptr; x int}, y int) int {
return closure.x + y
}
// 呼び出し
add5(3)
// ↓ コンパイラが変換
closure_wrapper(add5, 3)
; makeAdder のアセンブリ(簡略化)
makeAdder:
; クロージャ構造体を割り当て(16バイト)
mov rdi, 16
call runtime.newobject
; fn フィールドをセット
lea rbx, closure_wrapper
mov [rax], rbx
; x フィールドをセット(引数xをコピー)
mov [rax+8], rdi
; クロージャポインタを返す
ret
; closure_wrapper
closure_wrapper:
; RAX = クロージャポインタ
; RBX = y引数
mov rcx, [rax+8] ; x をロード
add rcx, rbx ; x + y
mov rax, rcx ; 結果を返す
ret
5.4 複数変数のキャプチャ
func makeMultiplier(factor int) func(int) int {
callCount := 0
return func(x int) int {
callCount++
fmt.Printf("Call #%d\n", callCount)
return x * factor
}
}
mul3 := makeMultiplier(3)
メモリレイアウト:
ヒープ上のクロージャ構造:
┌────────────────────┐
│ funcval │
│ fn: 0x... │ → wrapper関数
├────────────────────┤
│ キャプチャ変数: │
│ factor: 3 │
│ callCount: 0 │
└────────────────────┘
呼び出しごとに:
mul3(10):
- クロージャポインタをRAXへ
- [RAX+16] (callCount) を読み込み
- インクリメント
- [RAX+16] に書き戻し
- [RAX+8] (factor) を読み込み
- x * factor を計算
5.5 クロージャのパフォーマンス
⚠️ コスト:
1. ヒープ割り当て:
- 各クロージャ作成で malloc
- GC の対象
2. 間接呼び出し:
- 関数ポインタ経由の呼び出し
- インライン化が困難
3. ポインタ参照:
- キャプチャ変数へのアクセスは常にメモリ経由
ベンチマーク例:
// 通常の関数
func normalAdd(x, y int) int {
return x + y
}
// クロージャ
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func BenchmarkNormal(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = normalAdd(5, 3)
}
}
func BenchmarkClosure(b *testing.B) {
add := makeAdder(5)
for i := 0; i < b.N; i++ {
_ = add(3)
}
}
結果(目安):
BenchmarkNormal-8 1000000000 0.25 ns/op
BenchmarkClosure-8 500000000 0.80 ns/op
クロージャは約3倍遅い(それでも十分速い)
原因:
- 通常: 直接呼び出し、インライン化可能
- クロージャ: 間接呼び出し、メモリアクセス
💡 最適化のヒント:
// ❌ ループ内で毎回クロージャ作成
for i := 0; i < 1000000; i++ {
f := makeAdder(5) // 100万回の割り当て
result := f(i)
}
// ✅ ループ外で一度だけ作成
f := makeAdder(5)
for i := 0; i < 1000000; i++ {
result := f(i) // 割り当てなし
}
// ✅ クロージャを避ける
for i := 0; i < 1000000; i++ {
result := i + 5 // 最も速い
}
5.6 実用例: イベントリスナー
type EventListener func(string)
type EventEmitter struct {
listeners []EventListener
}
func (e *EventEmitter) On(listener EventListener) {
e.listeners = append(e.listeners, listener)
}
func (e *EventEmitter) Emit(event string) {
for _, listener := range e.listeners {
listener(event)
}
}
// 使用例
func main() {
emitter := &EventEmitter{}
// クロージャでリスナーを追加
counter := 0
emitter.On(func(event string) {
counter++
fmt.Printf("Event %d: %s\n", counter, event)
})
emitter.Emit("start")
emitter.Emit("process")
emitter.Emit("end")
}
6. defer の実装
6.1 defer の基本
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 関数終了時に実行
// ファイル処理
data, err := io.ReadAll(file)
if err != nil {
return err // この前に file.Close() が実行される
}
fmt.Println(string(data))
return nil // この前に file.Close() が実行される
}
6.2 defer チェインの実装
🔑 重要: defer は LIFO(後入れ先出し)のスタックで管理されます。
func example() {
defer fmt.Println("First")
defer fmt.Println("Second")
defer fmt.Println("Third")
fmt.Println("Main")
}
// 出力:
// Main
// Third ← 最後のdeferが最初に実行
// Second
// First ← 最初のdeferが最後に実行
defer チェインの構造:
type _defer struct {
siz int32 // 引数サイズ
started bool // 実行開始フラグ
sp uintptr // スタックポインタ
pc uintptr // プログラムカウンタ
fn *funcval // defer される関数
_panic *_panic // 関連する panic
link *_defer // 次の defer(リンクリスト)
}
Goroutine構造体:
┌──────────────────┐
│ Goroutine │
│ _defer: ────┐ │
└───────────────┼──┘
│
┌───────────────▼──┐
│ _defer #3 │
│ fn: println │
│ arg: "Third" │
│ link: ───┐ │
└────────────┼─────┘
│
┌────────────▼─────┐
│ _defer #2 │
│ fn: println │
│ arg: "Second" │
│ link: ───┐ │
└────────────┼─────┘
│
┌────────────▼─────┐
│ _defer #1 │
│ fn: println │
│ arg: "First" │
│ link: nil │
└──────────────────┘
実行時の動作:
example() 実行:
[1] defer fmt.Println("First") に到達
- _defer構造体を作成
- Goroutineのdeferチェインに追加
[2] defer fmt.Println("Second") に到達
- _defer構造体を作成
- チェインの先頭に追加
[3] defer fmt.Println("Third") に到達
- _defer構造体を作成
- チェインの先頭に追加
[4] fmt.Println("Main") 実行
出力: "Main"
[5] 関数終了前(RET命令前)
- deferチェインを先頭から実行
while g._defer != nil {
d := g._defer
g._defer = d.link
d.fn() // deferされた関数を呼び出し
}
実行順: Third → Second → First
6.3 defer のアセンブリ実装
func example() {
defer println("deferred")
println("normal")
}
example:
; プロローグ
push rbp
mov rbp, rsp
sub rsp, 32
; defer println("deferred") の登録
mov rdi, 16 ; _defer構造体のサイズ
call runtime.deferproc
test rax, rax
jnz .Ldefer_exit
; println("normal") の実行
lea rax, [.Lstr_normal]
call runtime.println
; 通常の終了
call runtime.deferreturn ; deferを実行
add rsp, 32
pop rbp
ret
.Ldefer_exit:
; deferproc がエラーを返した場合
call runtime.deferreturn
add rsp, 32
pop rbp
ret
.Lstr_normal: .ascii "normal"
.Lstr_deferred: .ascii "deferred"
6.4 defer のパフォーマンス影響
⚠️ コスト:
Go 1.13以前:
- 各deferで _defer構造体をヒープ割り当て
- チェイン管理のオーバーヘッド
- コスト: 約50ns/defer
Go 1.13-1.21:
- スタックベースの最適化(条件付き)
- シンプルなdeferはスタック上に配置
- コスト: 約10-20ns/defer
Go 1.22+:
- さらなる最適化
- インライン展開の改善
- コスト: 約5-10ns/defer
ベンチマーク:
func withDefer() {
defer func() {}()
// 処理
}
func withoutDefer() {
// 処理
}
func BenchmarkWithDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
withDefer()
}
}
func BenchmarkWithoutDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
withoutDefer()
}
}
結果(Go 1.22):
BenchmarkWithDefer-8 100000000 10.5 ns/op
BenchmarkWithoutDefer-8 1000000000 0.8 ns/op
defer のオーバーヘッド: 約10ns
実用上の影響:
- ファイルI/O: 1-10ms → deferは無視できる
- ネットワーク: 10-100ms → deferは無視できる
- 超高速ループ: 1ns → deferは重要(避けるべき)
💡 最適化のヒント:
// ❌ ホットパス(高頻度実行)でのdefer
func process(data []int) int {
mu.Lock()
defer mu.Unlock() // 毎回10nsのオーバーヘッド
sum := 0
for _, v := range data {
sum += v
}
return sum
}
// ✅ 手動でUnlock(クリティカルな場合のみ)
func process(data []int) int {
mu.Lock()
sum := 0
for _, v := range data {
sum += v
}
mu.Unlock()
return sum
}
// ⚠️ しかし、deferの方が安全(パニック時も確実に解放)
// パフォーマンスより安全性を優先すべき場合が多い
6.5 defer と panic/recover
func recoverExample() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("something went wrong")
}
panic発生時の動作:
[1] panic("...") 実行
- ランタイムがpanic処理を開始
[2] 現在のGoroutineのdeferチェインを実行
while g._defer != nil {
d := g._defer
g._defer = d.link
d.fn() // この中でrecoverが呼ばれるかも
if recovered {
// panic終了、通常実行に復帰
goto normal_flow
}
}
[3] 全deferを実行してもrecoverされなければ
- Goroutine終了
- プログラムクラッシュ(mainの場合)
_panic構造体:
┌──────────────────┐
│ _panic │
│ arg: "..." │ ← panicの引数
│ recovered: no │ ← recoverフラグ
│ _defer: ... │ ← 関連defer
└──────────────────┘
7. メソッドの内部実装
7.1 メソッドの基本
// 型定義
type Rectangle struct {
Width float64
Height float64
}
// メソッド(値レシーバ)
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// 使用例
rect := Rectangle{Width: 10, Height: 5}
fmt.Println("Area:", rect.Area()) // 50
fmt.Println("Perimeter:", rect.Perimeter()) // 30
7.2 レシーバの扱い
🔑 重要: メソッドは「第1引数がレシーバの関数」として実装されます。
// メソッド定義
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// コンパイラが変換する内部表現(疑似コード)
func Rectangle_Area(r Rectangle) float64 {
return r.Width * r.Height
}
// 呼び出し
rect.Area()
// ↓ コンパイラが変換
Rectangle_Area(rect)
メソッド呼び出しの変換:
ソースコード:
rect := Rectangle{Width: 10, Height: 5}
area := rect.Area()
コンパイラが生成:
rect := Rectangle{Width: 10, Height: 5}
area := Rectangle.Area(rect)
↑ レシーバが第1引数
7.3 値レシーバ vs ポインタレシーバ
// 値レシーバ
func (r Rectangle) AreaValue() float64 {
r.Width = 999 // コピーを変更(元は変わらない)
return r.Width * r.Height
}
// ポインタレシーバ
func (r *Rectangle) AreaPointer() float64 {
r.Width = 999 // 元を変更
return r.Width * r.Height
}
メモリレイアウトと動作:
[値レシーバ]
呼び出し前:
┌───────────────┐
│ rect │
│ Width: 10 │
│ Height: 5 │
└───────────────┘
rect.AreaValue() 呼び出し:
┌───────────────┐
│ rect │ ← 元のまま
│ Width: 10 │
│ Height: 5 │
└───────────────┘
スタック:
┌───────────────┐
│ r (コピー) │
│ Width: 999 │ ← メソッド内で変更
│ Height: 5 │
└───────────────┘
呼び出し後:
rect.Width == 10 // 変更されていない
[ポインタレシーバ]
呼び出し前:
┌───────────────┐
│ rect │
│ Width: 10 │
│ Height: 5 │
└───────────────┘
rect.AreaPointer() 呼び出し:
スタック:
┌───────────────┐
│ r (ポインタ) │ ─┐
└───────────────┘ │
│
ヒープ/スタック: │
┌───────────────┐ │
│ rect │ ←┘
│ Width: 999 │ ← 直接変更
│ Height: 5 │
└───────────────┘
呼び出し後:
rect.Width == 999 // 変更されている
アセンブリレベルの違い:
; 値レシーバ: rect.AreaValue()
; Rectangle は 16バイト(float64 × 2)
AreaValue:
; レシーバをコピー
mov rax, [rect] ; Width をコピー
mov rbx, [rect+8] ; Height をコピー
; 処理(コピーを変更)
mov rax, 999
mulsd xmm0, xmm1 ; Width * Height
ret
; ポインタレシーバ: rect.AreaPointer()
AreaPointer:
; RAX = レシーバのアドレス
; 処理(ポインタ経由で変更)
mov QWORD [rax], 999 ; rect.Width = 999
movsd xmm0, [rax] ; Width をロード
movsd xmm1, [rax+8] ; Height をロード
mulsd xmm0, xmm1 ; Width * Height
ret
7.4 自動的なポインタ変換
💡 Goの便利機能: 値とポインタは自動的に変換されます。
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) AreaValue() float64 {
return r.Width * r.Height
}
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
// 使用例
rect := Rectangle{Width: 10, Height: 5}
// 値レシーバ: そのまま呼び出せる
area := rect.AreaValue()
// ポインタレシーバ: 値からでも呼び出せる
rect.Scale(2) // コンパイラが (&rect).Scale(2) に変換
// ポインタからも呼び出せる
ptr := &rect
ptr.AreaValue() // コンパイラが (*ptr).AreaValue() に変換
ptr.Scale(2) // そのまま
コンパイラの変換:
ソースコード:
rect.Scale(2)
レシーバが値、メソッドがポインタ → アドレスを取る:
(&rect).Scale(2)
ソースコード:
ptr.AreaValue()
レシーバがポインタ、メソッドが値 → デリファレンス:
(*ptr).AreaValue()
⚠️ 注意: 変換できない場合
// マップの値は直接アドレスが取れない
m := map[string]Rectangle{
"key": Rectangle{10, 5},
}
// ❌ コンパイルエラー
m["key"].Scale(2) // cannot take address of m["key"]
// ✅ 修正版
r := m["key"]
r.Scale(2)
m["key"] = r
7.5 どちらを使うべきか
// ポインタレシーバを使うべき場合:
// 1. レシーバを変更する
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
// 2. 大きな構造体のコピーを避ける
type LargeStruct struct {
data [1000000]int
}
func (l *LargeStruct) Process() {
// コピーコスト(8MB)を避ける
}
// 3. 一貫性のため(同じ型のメソッドは統一)
type Counter struct {
count int
}
func (c *Counter) Increment() { c.count++ }
func (c *Counter) Value() int { return c.count } // 変更しないが、ポインタで統一
コピーコストの比較:
小さな構造体(16バイト):
値レシーバ: 2 CPU命令(MOV × 2)
ポインタレシーバ: 1 CPU命令(MOV ptr)
差: ほぼ無視できる
中程度の構造体(128バイト):
値レシーバ: 16 CPU命令(MOV × 16)
ポインタレシーバ: 1 CPU命令
差: 10-20ns
大きな構造体(8MB):
値レシーバ: 約2ms(メモリコピー)
ポインタレシーバ: 1ns(ポインタのみ)
差: 2,000,000倍!
💡 ガイドライン:
// ✅ 値レシーバを使う場合:
// - 構造体が小さい(3フィールド以下、24バイト以下)
// - 変更しない
// - プリミティブ型のエイリアス
type Point struct {
X, Y int
}
func (p Point) Distance() float64 {
return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
// ✅ ポインタレシーバを使う場合:
// - 変更する
// - 構造体が大きい
// - 同じ型の他のメソッドがポインタレシーバ
type Database struct {
connection *sql.DB
cache map[string]interface{}
config Config
}
func (db *Database) Query(sql string) (*Result, error) {
// ...
}
7.6 メソッドセットとインターフェース
🔑 重要: 値とポインタでメソッドセットが異なります。
type Shape interface {
Area() float64
}
type Scalable interface {
Scale(float64)
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
メソッドセット:
Rectangle(値)のメソッドセット:
- Area() ← 値レシーバ
*Rectangle(ポインタ)のメソッドセット:
- Area() ← 値レシーバ(自動で使える)
- Scale() ← ポインタレシーバ
インターフェース実装:
var s Shape
s = Rectangle{10, 5} // ✅ OK: Area()がある
s = &Rectangle{10, 5} // ✅ OK: Area()がある
var sc Scalable
sc = Rectangle{10, 5} // ❌ エラー: Scale()がない
sc = &Rectangle{10, 5} // ✅ OK: Scale()がある
理由:
値からはアドレスを取れないため:
Rectangle の Scale() を呼ぶには:
rect.Scale(2)
↓
(&rect).Scale(2) ← アドレスが必要
しかし、インターフェース経由では:
var sc Scalable = Rectangle{10, 5}
sc.Scale(2)
sc はインターフェース値(コピー)
コピーのアドレスを取っても元は変わらない
→ 意図しない動作を防ぐため禁止
7.7 メソッドチェーン
type StringBuilder struct {
buffer []string
}
func NewStringBuilder() *StringBuilder {
return &StringBuilder{
buffer: make([]string, 0),
}
}
// メソッドチェーン用にポインタを返す
func (sb *StringBuilder) Append(s string) *StringBuilder {
sb.buffer = append(sb.buffer, s)
return sb
}
func (sb *StringBuilder) AppendLine(s string) *StringBuilder {
sb.buffer = append(sb.buffer, s+"\n")
return sb
}
func (sb *StringBuilder) String() string {
return strings.Join(sb.buffer, "")
}
// 使用例
result := NewStringBuilder().
Append("Hello").
Append(", ").
Append("World").
AppendLine("!").
AppendLine("From Go").
String()
メソッドチェーンの内部動作:
NewStringBuilder() の戻り値を tmp1 として:
tmp1 := NewStringBuilder()
tmp2 := tmp1.Append("Hello") // tmp2 == tmp1
tmp3 := tmp2.Append(", ") // tmp3 == tmp2 == tmp1
tmp4 := tmp3.Append("World") // tmp4 == tmp3 == tmp2 == tmp1
tmp5 := tmp4.AppendLine("!") // tmp5 == ...
tmp6 := tmp5.AppendLine("From Go")
result := tmp6.String()
全て同じ *StringBuilder を指している
7.8 組み込み型へのメソッド追加
// 型エイリアスを作ってメソッドを追加
type MyInt int
func (m MyInt) Double() MyInt {
return m * 2
}
func (m MyInt) IsEven() bool {
return m%2 == 0
}
// 使用例
var num MyInt = 5
fmt.Println(num.Double()) // 10
fmt.Println(num.IsEven()) // false
var num2 MyInt = 6
fmt.Println(num2.IsEven()) // true
8. 関数とメソッドのベストプラクティス
8.1 短く保つ
// ✅ 良い例: 単一責任
func validateEmail(email string) error {
if !strings.Contains(email, "@") {
return errors.New("invalid email: missing @")
}
return nil
}
func sendEmail(to, subject, body string) error {
if err := validateEmail(to); err != nil {
return err
}
// メール送信ロジック
return nil
}
8.2 エラーを早く返す
// ✅ 良い例: アーリーリターン
func processFile(filename string) error {
if filename == "" {
return errors.New("filename is empty")
}
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return err
}
// 処理
return nil
}
8.3 インターフェースを受け取り、構造体を返す
// ✅ 良い例
type Reader interface {
Read([]byte) (int, error)
}
// インターフェースを受け取る
func processData(r Reader) (*Result, error) {
// 処理
return &Result{}, nil
}
// 具体的な型を返す
func NewProcessor() *Processor {
return &Processor{}
}
9. 自己確認問題
以下の質問に答えて、理解度を確認しましょう。
基本問題
- CALL命令は内部で何をしますか?
- スタックフレームの主要な構成要素を3つ挙げてください。
- Go 1.17以降、整数引数は最初にどのレジスタに配置されますか?
- 複数戻り値の場合、最初の2つはどのレジスタに配置されますか?
- クロージャがキャプチャする変数は、スタックとヒープのどちらに配置されますか?なぜですか?
- defer文のオーバーヘッドは約何ナノ秒ですか(Go 1.22)?
- 次のコードで、rect.Widthの値は何になりますか?
応用問題
rect := Rectangle{Width: 10, Height: 5}
rect.AreaValue() // func (r Rectangle) AreaValue() { r.Width = 999; ... }
fmt.Println(rect.Width)
- 値レシーバとポインタレシーバ、どちらを選ぶべき基準を3つ挙げてください。
- 次のコードはコンパイルエラーになります。なぜですか?
発展問題
type Scalable interface {
Scale(float64)
}
type Rectangle struct { Width, Height float64 }
func (r *Rectangle) Scale(f float64) { r.Width *= f; r.Height *= f }
var s Scalable = Rectangle{10, 5} // エラー
- defer文が複数ある場合、実行順序はどうなりますか?内部のデータ構造の名前も答えてください。
- レジスタ渡しとスタック渡し、なぜレジスタの方が高速ですか?具体的な理由を2つ挙げてください。
- 次のコードのクロージャは何バイトのメモリを消費しますか?(64bit環境)
func makeCounter() func() int {
count := 0
total := 0
return func() int {
count++
total += count
return total
}
}
実践問題
- 次のコードのベンチマーク結果を予想してください:
func normalFunc() int { return 42 }
func makeClosure() func() int {
x := 42
return func() int { return x }
}
// どちらが速い?何倍?
- Cgoを使った関数呼び出しは、純粋なGo関数呼び出しより約何倍遅いですか?
- 次のコードを最適化してください(パフォーマンス重視):
func process(items []int) {
for _, item := range items {
mu.Lock()
defer mu.Unlock()
// 処理
}
}
10. まとめ
この章では、Goの関数とメソッドの内部実装まで深く学びました。
重要ポイント
🔑 関数呼び出し:
- CALL命令が戻りアドレスをPUSH
- スタックフレームで引数・ローカル変数を管理
- RET命令で戻りアドレスをPOPして復帰
🔑 Go ABI:
- Go 1.17以降はレジスタベース
- 整数9個、浮動小数点15個までレジスタ
- レジスタ渡しで20-30%高速化
🔑 クロージャ:
- キャプチャ変数はヒープにエスケープ
- 関数値は構造体として実装
- 間接呼び出しで若干のオーバーヘッド
🔑 defer:
- LIFOスタック(後入れ先出し)
- 約10nsのオーバーヘッド(Go 1.22)
- panic時も確実に実行される
🔑 メソッド:
- 第1引数がレシーバの関数として実装
- 値レシーバはコピー、ポインタレシーバは参照
- 値とポインタのメソッドセットは異なる
Goらしい書き方
💡 ベストプラクティス:
- エラーを返して適切に処理
- 値レシーバは不変操作、ポインタレシーバは変更操作
- 短く焦点を絞った関数
- インターフェースを受け取り、構造体を返す
- deferで確実にリソース解放
⚠️ 避けるべきパターン:
- 長い関数(50行以上)
- 裸のreturnの乱用
- ホットパスでの不要なdefer
- 大きな構造体の値レシーバ
次のステップ
次の章では、配列とスライスについて学びます。メモリレイアウトから内部実装まで、徹底的に理解していきましょう。