Day 5: 関数 - 解説

学習の目的

  • 処理を再利用可能にする: 同じコードを何度も書かない(DRY原則)
  • コードを整理する: 機能ごとに分割(単一責任の原則)
  • 複数の値を返す: Goの特徴的な機能(エラーハンドリングパターン)
  • 抽象化を理解する: 実装の詳細を隠蔽し、インターフェースを公開

---

メンタルモデル:関数とは何か

関数は「ブラックボックス」

関数を理解する最も直感的な方法は、それを「ブラックボックス」として考えることです。

入力(引数) → [関数(ブラックボックス)] → 出力(戻り値)

実例で考える:

// 自動販売機のメンタルモデル
// 入力: お金 + 商品選択
// 処理: 内部メカニズム(見えない)
// 出力: 商品 + お釣り

func vendingMachine(money int, itemCode string) (string, int) {
    // 内部処理は利用者には見えない
    item := getItem(itemCode)
    change := money - item.price
    return item.name, change
}

この考え方により:

  • カプセル化: 内部実装を隠せる
  • 再利用: 同じ処理を何度でも呼び出せる
  • テスト: 入力と出力だけを検証すればよい

---

詳細なアルゴリズム分析

1. 関数呼び出しのスタックフレーム

関数が呼び出されると、コールスタックに「スタックフレーム」が積まれます。

┌─────────────────┐
│   main()        │ ← 最初に呼ばれる
├─────────────────┤
│   greet("太郎") │ ← main()から呼ばれる
├─────────────────┤
│   format(...)   │ ← greet()から呼ばれる
└─────────────────┘
   コールスタック

実装例:

package main

import "fmt"

// レベル3: 最も深い関数
func formatMessage(name string) string {
    return fmt.Sprintf("こんにちは、%sさん!", name)
}

// レベル2: 中間の関数
func greet(name string) {
    message := formatMessage(name) // formatMessage()を呼ぶ
    fmt.Println(message)
}

// レベル1: エントリーポイント
func main() {
    greet("太郎") // greet()を呼ぶ
}

スタックの動き:

1. main()がスタックに積まれる
2. greet("太郎")が呼ばれ、スタックに積まれる
3. formatMessage("太郎")が呼ばれ、スタックに積まれる
4. formatMessage()が完了し、スタックから取り除かれる
5. greet()が完了し、スタックから取り除かれる
6. main()が完了し、プログラム終了

時間計算量: O(1) - 関数呼び出し自体は定数時間 空間計算量: O(n) - 呼び出しの深さに比例(スタックオーバーフローに注意)

---

2. 再帰関数の計算量分析

階乗の計算(ナイーブな再帰):

func factorial(n int) int {
    // ベースケース
    if n == 0 {
        return 1
    }
    // 再帰ケース
    return n * factorial(n-1)
}

実行トレース(n=5の場合):

factorial(5)
= 5 * factorial(4)
= 5 * (4 * factorial(3))
= 5 * (4 * (3 * factorial(2)))
= 5 * (4 * (3 * (2 * factorial(1))))
= 5 * (4 * (3 * (2 * (1 * factorial(0)))))
= 5 * (4 * (3 * (2 * (1 * 1))))
= 5 * (4 * (3 * (2 * 1)))
= 5 * (4 * (3 * 2))
= 5 * (4 * 6)
= 5 * 24
= 120

計算量分析:

  • 時間計算量: O(n) - n回の関数呼び出し
  • 空間計算量: O(n) - コールスタックの深さ

改善版(末尾再帰):

func factorialTail(n int, acc int) int {
    if n == 0 {
        return acc
    }
    // 累積値を渡すことで、戻り時の計算を不要に
    return factorialTail(n-1, n*acc)
}

func factorial(n int) int {
    return factorialTail(n, 1)
}

改善点:

  • Goコンパイラは末尾再帰最適化をしないが、コンセプトとして重要
  • 他言語(例:関数型言語)では末尾再帰が自動的にループに最適化される

---

3. フィボナッチ数列:計算量の罠

ナイーブな実装:

func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}

計算木(n=5の場合):

                    fib(5)
                   /      \
              fib(4)      fib(3)
             /     \      /     \
        fib(3)   fib(2) fib(2) fib(1)
        /   \    /   \  /   \
    fib(2) fib(1)...
    /   \
fib(1) fib(0)

問題点:

  • 時間計算量: O(2^n) - 指数時間(非常に遅い)
  • 空間計算量: O(n) - 最大スタック深度
  • 重複計算: fib(3)、fib(2)などが何度も計算される

最適化版(メモ化):

func fibonacciMemo(n int, memo map[int]int) int {
    // すでに計算済みならメモから返す
    if val, ok := memo[n]; ok {
        return val
    }

    // ベースケース
    if n <= 1 {
        return n
    }

    // 計算してメモに保存
    memo[n] = fibonacciMemo(n-1, memo) + fibonacciMemo(n-2, memo)
    return memo[n]
}

func fibonacci(n int) int {
    memo := make(map[int]int)
    return fibonacciMemo(n, memo)
}

改善後の計算量:

  • 時間計算量: O(n) - 各値を1回だけ計算
  • 空間計算量: O(n) - メモ用のマップ + スタック

さらなる最適化(反復版):

func fibonacciIterative(n int) int {
    if n <= 1 {
        return n
    }

    prev, curr := 0, 1
    for i := 2; i <= n; i++ {
        prev, curr = curr, prev+curr
    }
    return curr
}

最終的な計算量:

  • 時間計算量: O(n)
  • 空間計算量: O(1) - 定数空間のみ使用

---

設計原則による関数設計

1. SOLID原則の適用

S - 単一責任の原則(Single Responsibility Principle)

悪い例:

// この関数は複数の責任を持つ
func processUser(name string, email string) {
    // 1. バリデーション
    if len(name) == 0 {
        fmt.Println("名前が空です")
        return
    }

    // 2. データ変換
    name = strings.TrimSpace(name)
    email = strings.ToLower(email)

    // 3. データベース保存
    db.Save(name, email)

    // 4. メール送信
    sendWelcomeEmail(email)

    // 5. ログ出力
    fmt.Printf("ユーザー登録完了: %s\n", name)
}

良い例:

// 各関数が単一の責任を持つ
func validateUser(name, email string) error {
    if len(name) == 0 {
        return fmt.Errorf("名前が空です")
    }
    if !strings.Contains(email, "@") {
        return fmt.Errorf("メールアドレスが無効です")
    }
    return nil
}

func normalizeUser(name, email string) (string, string) {
    name = strings.TrimSpace(name)
    email = strings.ToLower(email)
    return name, email
}

func saveUser(name, email string) error {
    return db.Save(name, email)
}

func notifyUser(email string) error {
    return sendWelcomeEmail(email)
}

// 統合関数
func processUser(name, email string) error {
    // バリデーション
    if err := validateUser(name, email); err != nil {
        return err
    }

    // 正規化
    name, email = normalizeUser(name, email)

    // 保存
    if err := saveUser(name, email); err != nil {
        return err
    }

    // 通知
    if err := notifyUser(email); err != nil {
        return err
    }

    log.Printf("ユーザー登録完了: %s\n", name)
    return nil
}

---

2. DRY原則(Don't Repeat Yourself)

悪い例:

func calculateRectangleArea(width, height float64) float64 {
    return width * height
}

func calculateSquareArea(side float64) float64 {
    return side * side  // 重複:長方形の面積計算と同じ
}

func calculateTriangleArea(base, height float64) float64 {
    return (base * height) / 2
}

良い例:

// 基本的な面積計算関数
func calculateArea(width, height float64) float64 {
    return width * height
}

// 高レベル関数は基本関数を再利用
func calculateRectangleArea(width, height float64) float64 {
    return calculateArea(width, height)
}

func calculateSquareArea(side float64) float64 {
    return calculateArea(side, side)
}

func calculateTriangleArea(base, height float64) float64 {
    return calculateArea(base, height) / 2
}

---

3. KISS原則(Keep It Simple, Stupid)

悪い例(過度に複雑):

func add(a, b int) int {
    // 不要な複雑性
    result := 0
    for i := 0; i < b; i++ {
        result = increment(result + a - a)
        result = increment(result)
    }
    return result
}

func increment(n int) int {
    return n + 1
}

良い例(シンプル):

func add(a, b int) int {
    return a + b
}

---

4. YAGNI原則(You Aren't Gonna Need It)

悪い例(不要な機能):

// 現在は2つの数の足し算しか必要ないのに...
func calculate(operation string, numbers ...float64) (float64, error) {
    switch operation {
    case "add":
        // 足し算の実装
    case "subtract":
        // 引き算の実装(まだ使われていない)
    case "multiply":
        // 掛け算の実装(まだ使われていない)
    case "divide":
        // 割り算の実装(まだ使われていない)
    case "power":
        // べき乗の実装(まだ使われていない)
    default:
        return 0, fmt.Errorf("未知の操作")
    }
    // ...
}

良い例(必要なものだけ):

// 今必要なのは足し算だけ
func add(a, b float64) float64 {
    return a + b
}

// 必要になったら他の操作を追加

---

概念の深掘り

値渡しと参照渡し(詳細版)

値渡しの仕組み:

func modifyValue(x int) {
    fmt.Printf("関数内のアドレス: %p\n", &x)
    x = x * 2
    fmt.Printf("関数内の値: %d\n", x)
}

func main() {
    n := 5
    fmt.Printf("main内のアドレス: %p\n", &n)
    fmt.Printf("main内の値(変更前): %d\n", n)

    modifyValue(n)

    fmt.Printf("main内の値(変更後): %d\n", n)
}

出力:

main内のアドレス: 0xc000014088
main内の値(変更前): 5
関数内のアドレス: 0xc0000140a0    ← 異なるアドレス!
関数内の値: 10
main内の値(変更後): 5           ← 元の値は変わらない

メモリ図:

main()のスタックフレーム:
┌─────────┐
│ n = 5   │ ← アドレス 0xc000014088
└─────────┘

modifyValue()のスタックフレーム:
┌─────────┐
│ x = 5   │ ← アドレス 0xc0000140a0(コピー)
│ ↓       │
│ x = 10  │ ← コピーを変更
└─────────┘

ポインタを使った参照渡し:

func modifyValueByPointer(x *int) {
    fmt.Printf("ポインタが指すアドレス: %p\n", x)
    *x = *x * 2  // ポインタを経由して元の値を変更
    fmt.Printf("変更後の値: %d\n", *x)
}

func main() {
    n := 5
    fmt.Printf("main内のアドレス: %p\n", &n)
    fmt.Printf("main内の値(変更前): %d\n", n)

    modifyValueByPointer(&n)  // アドレスを渡す

    fmt.Printf("main内の値(変更後): %d\n", n)  // 変更されている!
}

出力:

main内のアドレス: 0xc000014088
main内の値(変更前): 5
ポインタが指すアドレス: 0xc000014088  ← 同じアドレス!
変更後の値: 10
main内の値(変更後): 10            ← 元の値が変更された

---

スライスと値渡しの特殊性

スライスは「参照型っぽく」振る舞う:

func modifySlice(s []int) {
    s[0] = 999  // 元のスライスの要素が変更される!
    fmt.Println("関数内:", s)
}

func main() {
    numbers := []int{1, 2, 3}
    fmt.Println("変更前:", numbers)

    modifySlice(numbers)

    fmt.Println("変更後:", numbers)  // 変更されている!
}

出力:

変更前: [1 2 3]
関数内: [999 2 3]
変更後: [999 2 3]

理由: スライスは内部的に以下の構造を持つ:

type slice struct {
    ptr *element  // 配列へのポインタ
    len int       // 長さ
    cap int       // 容量
}

スライスのコピーでも同じ配列を参照:

main()のスライス:
┌─────────────────┐
│ ptr → [1,2,3]   │ ← 配列へのポインタ
│ len = 3         │
│ cap = 3         │
└─────────────────┘

modifySlice()のスライス(コピー):
┌─────────────────┐
│ ptr → [1,2,3]   │ ← 同じ配列を指す!
│ len = 3         │
│ cap = 3         │
└─────────────────┘

---

ビジュアル図解

1. 関数呼び出しフロー(ASCII Art)

          ┌─────────────────────────────────┐
          │     プログラム実行開始          │
          └────────────┬────────────────────┘
                       │
                       ▼
          ┌─────────────────────────────────┐
          │     main() 関数実行             │
          │  ┌─────────────────────────┐    │
          │  │  変数初期化              │    │
          │  │  n := 10                │    │
          │  └──────────┬──────────────┘    │
          │             │                    │
          │             ▼                    │
          │  ┌─────────────────────────┐    │
          │  │  double(n) を呼び出し   │────┼──┐
          │  └─────────────────────────┘    │  │
          └─────────────────────────────────┘  │
                                               │
                                               │ 制御が移る
                                               │
                       ┌───────────────────────┘
                       │
                       ▼
          ┌─────────────────────────────────┐
          │   double(x int) int 関数実行    │
          │  ┌─────────────────────────┐    │
          │  │  引数受け取り            │    │
          │  │  x = 10 (コピー)        │    │
          │  └──────────┬──────────────┘    │
          │             │                    │
          │             ▼                    │
          │  ┌─────────────────────────┐    │
          │  │  計算実行                │    │
          │  │  result = x * 2         │    │
          │  │  result = 20            │    │
          │  └──────────┬──────────────┘    │
          │             │                    │
          │             ▼                    │
          │  ┌─────────────────────────┐    │
          │  │  値を返す                │────┼──┐
          │  │  return 20              │    │  │
          │  └─────────────────────────┘    │  │
          └─────────────────────────────────┘  │
                                               │
                                               │ 戻り値と制御が戻る
                                               │
                       ┌───────────────────────┘
                       │
                       ▼
          ┌─────────────────────────────────┐
          │     main() 関数に戻る           │
          │  ┌─────────────────────────┐    │
          │  │  戻り値を受け取り        │    │
          │  │  result = 20            │    │
          │  └──────────┬──────────────┘    │
          │             │                    │
          │             ▼                    │
          │  ┌─────────────────────────┐    │
          │  │  結果を出力              │    │
          │  │  fmt.Println(result)    │    │
          │  └─────────────────────────┘    │
          └─────────────────────────────────┘

---

2. 再帰関数のスタック展開(Mermaid図)

graph TD
    A[factorial 5] -->|5 * factorial 4| B[factorial 4]
    B -->|4 * factorial 3| C[factorial 3]
    C -->|3 * factorial 2| D[factorial 2]
    D -->|2 * factorial 1| E[factorial 1]
    E -->|1 * factorial 0| F[factorial 0]
    F -->|return 1| E
    E -->|return 1| D
    D -->|return 2| C
    C -->|return 6| B
    B -->|return 24| A
    A -->|return 120| G[結果: 120]

---

3. 複数戻り値のパターン(図解)

┌─────────────────────────────────────────────────────────┐
│  関数: divide(a, b int) (int, error)                   │
└─────────────────────────────────────────────────────────┘
                         │
                         │ 入力: a=10, b=2
                         ▼
        ┌────────────────────────────────┐
        │     条件チェック                │
        │     b == 0 ?                   │
        └────────┬───────────────┬────────┘
                 │               │
           No    │               │ Yes
                 │               │
                 ▼               ▼
    ┌────────────────┐  ┌────────────────────┐
    │  正常パス      │  │   エラーパス       │
    │  result = a/b  │  │   return 0, error  │
    │  = 10/2 = 5    │  │                    │
    └────────┬───────┘  └────────┬───────────┘
             │                   │
             │                   │
             ▼                   ▼
    ┌────────────────┐  ┌────────────────────┐
    │  return 5, nil │  │  return 0, error   │
    └────────┬───────┘  └────────┬───────────┘
             │                   │
             └─────────┬─────────┘
                       │
                       ▼
        ┌──────────────────────────────┐
        │   呼び出し元でハンドリング   │
        │   result, err := divide(...) │
        │   if err != nil { ... }      │
        └──────────────────────────────┘

---

さらなる探求

1. 高階関数(Higher-Order Functions)

関数を引数として受け取ったり、関数を返したりする関数。

例: マップ関数の実装

// transform は各要素にfnを適用する高階関数
func transform(numbers []int, fn func(int) int) []int {
    result := make([]int, len(numbers))
    for i, n := range numbers {
        result[i] = fn(n)  // 関数を適用
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}

    // 2倍にする関数
    doubled := transform(numbers, func(n int) int {
        return n * 2
    })
    fmt.Println(doubled)  // [2 4 6 8 10]

    // 2乗する関数
    squared := transform(numbers, func(n int) int {
        return n * n
    })
    fmt.Println(squared)  // [1 4 9 16 25]
}

---

2. クロージャ(Closures)

外側の関数のスコープにある変数を「閉じ込める」関数。

例: カウンタージェネレーター

func makeCounter() func() int {
    count := 0  // この変数はクロージャに「捕捉」される
    return func() int {
        count++
        return count
    }
}

func main() {
    counter1 := makeCounter()
    counter2 := makeCounter()

    fmt.Println(counter1())  // 1
    fmt.Println(counter1())  // 2
    fmt.Println(counter1())  // 3

    fmt.Println(counter2())  // 1 (独立したカウンター)
    fmt.Println(counter2())  // 2
}

メモリ図:

counter1のクロージャ:
┌──────────────┐
│ count = 3    │ ← 独自のcount変数を保持
└──────────────┘

counter2のクロージャ:
┌──────────────┐
│ count = 2    │ ← 別の独立したcount変数
└──────────────┘

---

3. 可変長引数(Variadic Functions)

基本的な使い方:

func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3))           // 6
    fmt.Println(sum(1, 2, 3, 4, 5))     // 15
    fmt.Println(sum())                  // 0

    // スライスを展開して渡す
    nums := []int{10, 20, 30}
    fmt.Println(sum(nums...))           // 60
}

実用例: ログ関数

func logInfo(format string, args ...interface{}) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    message := fmt.Sprintf(format, args...)
    fmt.Printf("[INFO] %s: %s\n", timestamp, message)
}

func main() {
    logInfo("サーバー起動")
    logInfo("ユーザー接続: %s", "user@example.com")
    logInfo("処理完了: %d 件処理しました", 42)
}

---

4. 遅延実行(Defer)

基本的な使い方:

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()  // 関数終了時に必ず実行される

    // ファイル読み込み処理
    // ...

    return nil  // ここでfile.Close()が自動的に呼ばれる
}

deferのスタック動作:

func example() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    fmt.Println("通常の処理")
}

// 出力:
// 通常の処理
// 3  ← 後から登録されたものが先に実行される(LIFO)
// 2
// 1

実用例: トランザクション管理

func transferMoney(from, to string, amount float64) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }

    // 成功/失敗に関わらず、適切に処理
    defer func() {
        if err != nil {
            tx.Rollback()  // エラー時はロールバック
        } else {
            tx.Commit()    // 成功時はコミット
        }
    }()

    // 送金処理
    err = debit(tx, from, amount)
    if err != nil {
        return err
    }

    err = credit(tx, to, amount)
    if err != nil {
        return err
    }

    return nil
}

---

セルフチェック質問

理解度を確認するための質問です。答えられるか試してみましょう。

基礎レベル

  • 関数とは何ですか?3つの主要な要素を挙げてください。
解答例

関数は処理をまとめたコードブロックです。3つの主要要素: - 引数(パラメータ): 関数に渡す入力値 - 処理(本体): 実行される命令 - 戻り値(返り値): 関数から返される出力値

  • 値渡しと参照渡しの違いは何ですか?
解答例

- 値渡し: 値のコピーが渡される。元の変数は変更されない。 - 参照渡し: 変数のアドレス(ポインタ)が渡される。元の変数が変更される可能性がある。

Goはデフォルトで値渡しだが、ポインタを使うことで参照渡しの動作を実現できる。

  • なぜ関数を使うのですか?メリットを3つ挙げてください。
解答例

- 再利用性: 同じ処理を何度でも呼び出せる(DRY原則) - 保守性: コードを整理・分割できる(単一責任の原則) - テスト容易性: 独立した単位でテストできる

中級レベル

  • 以下のコードの時間計算量と空間計算量は?
   func fibonacci(n int) int {
       if n <= 1 {
           return n
       }
       return fibonacci(n-1) + fibonacci(n-2)
   }
   
解答例

- 時間計算量: O(2^n) - 指数時間(重複計算が多い) - 空間計算量: O(n) - 再帰の最大深度

メモ化や反復版に書き換えることで、時間計算量をO(n)に改善できる。

  • クロージャとは何ですか?どんな場面で有用ですか?
解答例

外側の関数のスコープにある変数を「捕捉」する関数。

有用な場面: - カウンター、ジェネレーター - イベントハンドラー - コールバック関数 - 状態を保持する必要がある場面

上級レベル

  • DRY原則とYAGNI原則のバランスをどう取りますか?
解答例

- DRY(Don't Repeat Yourself): 重複を避ける - YAGNI(You Aren't Gonna Need It): 不要な機能を作らない

バランスの取り方: - すでに2回以上使われている処理を関数化(DRY) - 将来使うかもしれない機能は作らない(YAGNI) - リファクタリング時に統合を検討

  • 末尾再帰最適化とは何ですか?Goで実現できますか?
解答例

末尾再帰最適化:再帰呼び出しが関数の最後にある場合、コンパイラがループに変換する最適化。

Goでは実現できない: Goコンパイラは末尾再帰最適化を行わない。

対策: - 明示的に反復版を書く - スタックオーバーフローに注意

---

明日への準備

Day 6は「配列とスライス」を学びます。

予習ポイント:

// 配列:固定長
arr := [3]int{1, 2, 3}

// スライス:可変長
slice := []int{1, 2, 3}
slice = append(slice, 4)  // 要素を追加できる

関数との関連:

  • スライスを関数の引数として渡す方法
  • スライスを返す関数の設計
  • スライスの内部構造(ポインタ、長さ、容量)

---

まとめ

概念 説明 計算量
関数呼び出し 処理をまとめたもの O(1)
引数 関数に渡す値(値渡し) -
戻り値 関数から返す値 -
再帰 自分自身を呼び出す O(n) ~ O(2^n)
高階関数 関数を引数/戻り値にする -
クロージャ 外側の変数を捕捉する -
defer 遅延実行(クリーンアップ) -

重要な設計原則:

  • 単一責任の原則: 1つの関数は1つのことだけをする
  • DRY原則: 同じコードを繰り返さない
  • KISS原則: シンプルに保つ
  • YAGNI原則: 不要な機能は作らない

次のステップ:

  • 関数を使った実装問題を解く
  • 計算量を意識したコーディング
  • 設計原則を実践する