Day 5: 関数 - 解答例

目次

---

課題1の解答

問題1-1: 挨拶関数

解法1: 基本的な実装

package main

import "fmt"

func greet(name string) {
    fmt.Printf("こんにちは、%sさん!\n", name)
}

func main() {
    greet("太郎")
    greet("花子")
}

解法2: 戻り値として文字列を返す

func greet(name string) string {
    return fmt.Sprintf("こんにちは、%sさん!", name)
}

func main() {
    message := greet("太郎")
    fmt.Println(message)

    // 複数の挨拶をまとめて処理
    names := []string{"太郎", "花子", "次郎"}
    for _, name := range names {
        fmt.Println(greet(name))
    }
}

利点:

  • テストしやすい
  • 出力方法を呼び出し側で制御できる
  • ログファイルへの書き込みなど、柔軟な使い方が可能

解法3: 時間帯に応じた挨拶

import (
    "fmt"
    "time"
)

func greetByTime(name string) string {
    hour := time.Now().Hour()
    var greeting string

    switch {
    case hour < 12:
        greeting = "おはようございます"
    case hour < 18:
        greeting = "こんにちは"
    default:
        greeting = "こんばんは"
    }

    return fmt.Sprintf("%s、%sさん!", greeting, name)
}

func main() {
    fmt.Println(greetByTime("太郎"))
}

---

問題1-2: 足し算関数

解法1: 基本的な実装

package main

import "fmt"

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

func main() {
    result := add(3, 5)
    fmt.Printf("3 + 5 = %d\n", result)
}

解法2: 可変長引数で複数の数値を加算

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

func main() {
    fmt.Printf("3 + 5 = %d\n", add(3, 5))
    fmt.Printf("1 + 2 + 3 + 4 + 5 = %d\n", add(1, 2, 3, 4, 5))
    fmt.Printf("10 + 20 + 30 = %d\n", add(10, 20, 30))
}

実用性: この実装は実際のプロジェクトでより有用

解法3: ジェネリクスを使った型安全な実装(Go 1.18+)

func add[T int | int64 | float64](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(add(3, 5))           // int
    fmt.Println(add(3.5, 2.1))       // float64
    fmt.Println(add[int64](100, 200)) // int64
}

解法4: エラーハンドリング付き

import "errors"

func addWithValidation(a, b int) (int, error) {
    // オーバーフローチェック
    if a > 0 && b > 0 && a > (math.MaxInt-b) {
        return 0, errors.New("integer overflow")
    }
    return a + b, nil
}

func main() {
    result, err := addWithValidation(3, 5)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("3 + 5 = %d\n", result)
}

---

問題1-3: 最大値関数

解法1: if文を使った基本実装

package main

import "fmt"

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Printf("max(3, 7) = %d\n", max(3, 7))
}

解法2: 三項演算子風(Goにはないがこう書ける)

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

Goには三項演算子がないため、これが最もシンプルな方法です。

解法3: 可変長引数で複数の値から最大値を見つける

import "errors"

func max(numbers ...int) (int, error) {
    if len(numbers) == 0 {
        return 0, errors.New("no numbers provided")
    }

    maxVal := numbers[0]
    for _, n := range numbers[1:] {
        if n > maxVal {
            maxVal = n
        }
    }
    return maxVal, nil
}

func main() {
    result, _ := max(3, 7, 2, 9, 1)
    fmt.Printf("max(3, 7, 2, 9, 1) = %d\n", result)
}

解法4: ジェネリクスを使った汎用実装

import "golang.org/x/exp/constraints"

func max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Println(max(3, 7))           // int
    fmt.Println(max(3.5, 2.1))       // float64
    fmt.Println(max("apple", "banana")) // string
}

---

課題2の解答

問題2-1: 割り算関数

解法1: 基本的な実装

package main

import "fmt"

func divide(a, b int) (int, int) {
    return a / b, a % b
}

func main() {
    q, r := divide(17, 5)
    fmt.Printf("17 ÷ 5 = %d 余り %d\n", q, r)
}

解法2: 名前付き戻り値を使った実装

func divide(a, b int) (quotient, remainder int) {
    quotient = a / b
    remainder = a % b
    return
}

func main() {
    q, r := divide(17, 5)
    fmt.Printf("17 ÷ 5 = %d 余り %d\n", q, r)
}

メリット: 戻り値の意味が明確になる

解法3: エラーハンドリング付き(本番環境推奨)

import (
    "errors"
    "fmt"
)

func divide(a, b int) (quotient, remainder int, err error) {
    if b == 0 {
        return 0, 0, errors.New("division by zero")
    }
    quotient = a / b
    remainder = a % b
    return quotient, remainder, nil
}

func main() {
    q, r, err := divide(17, 5)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("17 ÷ 5 = %d 余り %d\n", q, r)

    // ゼロ除算のテスト
    _, _, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    }
}

解法4: 構造体で結果を返す

type DivisionResult struct {
    Quotient  int
    Remainder int
    Error     error
}

func divide(a, b int) DivisionResult {
    if b == 0 {
        return DivisionResult{
            Error: errors.New("division by zero"),
        }
    }
    return DivisionResult{
        Quotient:  a / b,
        Remainder: a % b,
    }
}

func main() {
    result := divide(17, 5)
    if result.Error != nil {
        fmt.Println("Error:", result.Error)
        return
    }
    fmt.Printf("17 ÷ 5 = %d 余り %d\n", result.Quotient, result.Remainder)
}

---

問題2-2: 統計関数

解法1: 基本的な実装

package main

import "fmt"

func stats(nums []int) (int, float64) {
    sum := 0
    for _, n := range nums {
        sum += n
    }
    avg := float64(sum) / float64(len(nums))
    return sum, avg
}

func main() {
    numbers := []int{10, 20, 30, 40, 50}
    sum, avg := stats(numbers)
    fmt.Printf("合計: %d, 平均: %.1f\n", sum, avg)
}

解法2: エラーハンドリングと名前付き戻り値

import (
    "errors"
    "fmt"
)

func stats(nums []int) (sum int, avg float64, err error) {
    if len(nums) == 0 {
        return 0, 0, errors.New("empty slice")
    }

    for _, n := range nums {
        sum += n
    }
    avg = float64(sum) / float64(len(nums))
    return sum, avg, nil
}

func main() {
    numbers := []int{10, 20, 30, 40, 50}
    sum, avg, err := stats(numbers)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("合計: %d, 平均: %.2f\n", sum, avg)
}

解法3: より詳細な統計情報

import "math"

type Statistics struct {
    Sum     int
    Average float64
    Min     int
    Max     int
    Count   int
}

func stats(nums []int) (Statistics, error) {
    if len(nums) == 0 {
        return Statistics{}, errors.New("empty slice")
    }

    result := Statistics{
        Min:   nums[0],
        Max:   nums[0],
        Count: len(nums),
    }

    for _, n := range nums {
        result.Sum += n
        if n < result.Min {
            result.Min = n
        }
        if n > result.Max {
            result.Max = n
        }
    }

    result.Average = float64(result.Sum) / float64(result.Count)
    return result, nil
}

func main() {
    numbers := []int{10, 20, 30, 40, 50}
    stat, err := stats(numbers)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Printf("合計: %d\n", stat.Sum)
    fmt.Printf("平均: %.2f\n", stat.Average)
    fmt.Printf("最小: %d\n", stat.Min)
    fmt.Printf("最大: %d\n", stat.Max)
    fmt.Printf("個数: %d\n", stat.Count)
}

解法4: 標準偏差も計算する

import "math"

func statsAdvanced(nums []int) (sum int, avg, stdDev float64, err error) {
    if len(nums) == 0 {
        return 0, 0, 0, errors.New("empty slice")
    }

    // 合計と平均
    for _, n := range nums {
        sum += n
    }
    avg = float64(sum) / float64(len(nums))

    // 分散と標準偏差
    var variance float64
    for _, n := range nums {
        diff := float64(n) - avg
        variance += diff * diff
    }
    variance /= float64(len(nums))
    stdDev = math.Sqrt(variance)

    return sum, avg, stdDev, nil
}

---

課題3の解答

問題3-1: 再帰関数(階乗)

解法1: 基本的な再帰実装

func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

func main() {
    fmt.Println(factorial(5))  // 120
    fmt.Println(factorial(10)) // 3628800
}

解法2: エラーハンドリング付き

import "errors"

func factorial(n int) (int, error) {
    if n < 0 {
        return 0, errors.New("negative number not allowed")
    }
    if n <= 1 {
        return 1, nil
    }
    result, err := factorial(n - 1)
    if err != nil {
        return 0, err
    }
    return n * result, nil
}

解法3: 反復版(パフォーマンス重視)

func factorialIterative(n int) int {
    if n <= 1 {
        return 1
    }
    result := 1
    for i := 2; i <= n; i++ {
        result *= i
    }
    return result
}

パフォーマンス比較:

  • 再帰版: O(n)時間、O(n)スタック空間
  • 反復版: O(n)時間、O(1)スタック空間

解法4: メモ化による最適化

var factorialMemo = map[int]int{0: 1, 1: 1}

func factorialMemoized(n int) int {
    if val, ok := factorialMemo[n]; ok {
        return val
    }
    factorialMemo[n] = n * factorialMemoized(n-1)
    return factorialMemo[n]
}

解法5: 末尾再帰風(Goでは最適化されないが参考)

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

func factorialHelper(n, accumulator int) int {
    if n <= 1 {
        return accumulator
    }
    return factorialHelper(n-1, n*accumulator)
}

---

問題3-2: フィボナッチ

解法1: 基本的な再帰実装

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

func main() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", fibonacci(i))
    }
    // 出力: 0 1 1 2 3 5 8 13 21 34
}

警告: この実装は指数時間(O(2^n))で非効率

解法2: メモ化による最適化

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

func fibMemo(n int, memo map[int]int) int {
    if n <= 1 {
        return n
    }
    if val, ok := memo[n]; ok {
        return val
    }
    memo[n] = fibMemo(n-1, memo) + fibMemo(n-2, memo)
    return memo[n]
}

改善: O(n)時間に改善

解法3: 反復版(最も効率的)

func fibonacciIterative(n int) int {
    if n <= 1 {
        return n
    }
    a, b := 0, 1
    for i := 2; i <= n; i++ {
        a, b = b, a+b
    }
    return b
}

特徴:

  • O(n)時間
  • O(1)空間
  • 本番環境推奨

解法4: スライスを使ったメモ化

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

    fib := make([]int, n+1)
    fib[0] = 0
    fib[1] = 1

    for i := 2; i <= n; i++ {
        fib[i] = fib[i-1] + fib[i-2]
    }

    return fib[n]
}

解法5: チャネルを使った生成器パターン

func fibonacciGenerator(n int) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)
        a, b := 0, 1
        for i := 0; i < n; i++ {
            ch <- a
            a, b = b, a+b
        }
    }()
    return ch
}

func main() {
    for num := range fibonacciGenerator(10) {
        fmt.Printf("%d ", num)
    }
}

---

複数の解法比較

パフォーマンス比較表

関数 再帰版 反復版 メモ化版
階乗(20) 遅い 高速 高速
フィボナッチ(30) 非常に遅い 高速 高速
空間計算量 O(n) O(1) O(n)

---

エラーハンドリングの追加

全課題に対する堅牢な実装例

package main

import (
    "errors"
    "fmt"
)

// 入力検証付き挨拶関数
func greet(name string) (string, error) {
    if name == "" {
        return "", errors.New("name cannot be empty")
    }
    return fmt.Sprintf("こんにちは、%sさん!", name), nil
}

// オーバーフローチェック付き加算
func add(a, b int) (int, error) {
    if (b > 0 && a > (1<<63-1)-b) || (b < 0 && a < (-1<<63)-b) {
        return 0, errors.New("integer overflow")
    }
    return a + b, nil
}

// 安全な除算
func divide(a, b int) (quotient, remainder int, err error) {
    if b == 0 {
        return 0, 0, errors.New("division by zero")
    }
    return a / b, a % b, nil
}

// 安全な統計計算
func stats(nums []int) (sum int, avg float64, err error) {
    if len(nums) == 0 {
        return 0, 0, errors.New("empty slice")
    }
    for _, n := range nums {
        sum += n
    }
    avg = float64(sum) / float64(len(nums))
    return sum, avg, nil
}

---

テストコード

package main

import "testing"

func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive numbers", 3, 5, 8},
        {"negative numbers", -3, -5, -8},
        {"mixed", -3, 5, 2},
        {"zero", 0, 5, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

func TestDivide(t *testing.T) {
    q, r := divide(17, 5)
    if q != 3 || r != 2 {
        t.Errorf("divide(17, 5) = (%d, %d); want (3, 2)", q, r)
    }
}

func TestFactorial(t *testing.T) {
    tests := []struct {
        input int
        want  int
    }{
        {0, 1},
        {1, 1},
        {5, 120},
        {10, 3628800},
    }

    for _, tt := range tests {
        got := factorial(tt.input)
        if got != tt.want {
            t.Errorf("factorial(%d) = %d; want %d", tt.input, got, tt.want)
        }
    }
}

---

ベンチマーク

func BenchmarkFactorialRecursive(b *testing.B) {
    for i := 0; i < b.N; i++ {
        factorial(20)
    }
}

func BenchmarkFactorialIterative(b *testing.B) {
    for i := 0; i < b.N; i++ {
        factorialIterative(20)
    }
}

func BenchmarkFibonacciRecursive(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fibonacci(20)
    }
}

func BenchmarkFibonacciIterative(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fibonacciIterative(20)
    }
}

実行方法:

go test -bench=.

---

実践的な拡張

電卓プログラム(全機能統合)

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

type Calculator struct{}

func (c Calculator) Add(a, b float64) float64 {
    return a + b
}

func (c Calculator) Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

func (c Calculator) Factorial(n int) (int, error) {
    if n < 0 {
        return 0, fmt.Errorf("negative number")
    }
    result := 1
    for i := 2; i <= n; i++ {
        result *= i
    }
    return result, nil
}

func main() {
    calc := Calculator{}
    scanner := bufio.NewScanner(os.Stdin)

    fmt.Println("簡易電卓 - コマンド: add, div, fact, quit")

    for {
        fmt.Print("> ")
        if !scanner.Scan() {
            break
        }

        input := scanner.Text()
        parts := strings.Fields(input)

        if len(parts) == 0 {
            continue
        }

        switch parts[0] {
        case "add":
            if len(parts) != 3 {
                fmt.Println("使い方: add <数値1> <数値2>")
                continue
            }
            a, _ := strconv.ParseFloat(parts[1], 64)
            b, _ := strconv.ParseFloat(parts[2], 64)
            fmt.Printf("結果: %.2f\n", calc.Add(a, b))

        case "div":
            if len(parts) != 3 {
                fmt.Println("使い方: div <数値1> <数値2>")
                continue
            }
            a, _ := strconv.ParseFloat(parts[1], 64)
            b, _ := strconv.ParseFloat(parts[2], 64)
            if result, err := calc.Divide(a, b); err != nil {
                fmt.Println("エラー:", err)
            } else {
                fmt.Printf("結果: %.2f\n", result)
            }

        case "fact":
            if len(parts) != 2 {
                fmt.Println("使い方: fact <整数>")
                continue
            }
            n, _ := strconv.Atoi(parts[1])
            if result, err := calc.Factorial(n); err != nil {
                fmt.Println("エラー:", err)
            } else {
                fmt.Printf("結果: %d\n", result)
            }

        case "quit":
            fmt.Println("終了します")
            return

        default:
            fmt.Println("不明なコマンド")
        }
    }
}

---

まとめ

この解答例では以下を網羅しました:

  • 複数の解法: 各問題に2〜5つの異なるアプローチ
  • エラーハンドリング: 本番環境を想定した堅牢な実装
  • パフォーマンス: 再帰vs反復、メモ化などの最適化手法
  • テストとベンチマーク: 品質保証の実践
  • 実践的な統合例: 学んだ内容を活用したミニプロジェクト

次のステップ:

  • これらのコードを実際に動かしてみる
  • テストを書いて品質を確認
  • ベンチマークでパフォーマンスを測定
  • 独自の拡張機能を追加