Day 2: 変数と型 - 解答例

課題1の解答

問題1-1: 自己紹介プログラム

package main

import "fmt"

func main() {
    name := "太郎"
    age := 20
    hobby := "読書"

    fmt.Println("私の名前は" + name + "です")
    fmt.Printf("年齢は%d歳です\n", age)
    fmt.Println("趣味は" + hobby + "です")
}

別解(Printfのみ使用):

package main

import "fmt"

func main() {
    name := "太郎"
    age := 20
    hobby := "読書"

    fmt.Printf("私の名前は%sです\n", name)
    fmt.Printf("年齢は%d歳です\n", age)
    fmt.Printf("趣味は%sです\n", hobby)
}

出力:

私の名前は太郎です
年齢は20歳です
趣味は読書です

---

問題1-2: 果物の計算

package main

import "fmt"

func main() {
    apples := 3
    oranges := 5
    total := apples + oranges

    fmt.Printf("りんご: %d個\n", apples)
    fmt.Printf("みかん: %d個\n", oranges)
    fmt.Printf("合計: %d個\n", total)
}

出力:

りんご: 3個
みかん: 5個
合計: 8個

---

問題1-3: 値の更新

package main

import "fmt"

func main() {
    score := 80
    fmt.Printf("初期スコア: %d\n", score)

    score = score + 10  // または score += 10
    fmt.Printf("10点追加後: %d\n", score)

    score = score - 5   // または score -= 5
    fmt.Printf("5点減少後: %d\n", score)

    score = score * 2   // または score *= 2
    fmt.Printf("2倍後: %d\n", score)
}

出力:

初期スコア: 80
10点追加後: 90
5点減少後: 85
2倍後: 170

ポイント: 複合代入演算子を使った別解

score += 10  // score = score + 10 と同じ
score -= 5   // score = score - 5 と同じ
score *= 2   // score = score * 2 と同じ

---

課題2の解答

問題2-1: 様々な型

package main

import "fmt"

func main() {
    favoriteNumber := 42
    pi := 3.14159
    favoriteWord := "プログラミングは楽しい"
    isHappy := true

    fmt.Printf("好きな数字: %d\n", favoriteNumber)
    fmt.Printf("円周率: %f\n", pi)
    fmt.Printf("好きな言葉: %s\n", favoriteWord)
    fmt.Printf("真偽値: %t\n", isHappy)
}

出力:

好きな数字: 42
円周率: 3.141590
好きな言葉: プログラミングは楽しい
真偽値: true

小数点以下の桁数を指定する場合:

fmt.Printf("円周率: %.5f\n", pi)  // 3.14159(小数点以下5桁)

---

問題2-2: 整数の計算

package main

import "fmt"

func main() {
    a := 17
    b := 5

    fmt.Printf("a = %d, b = %d\n", a, b)
    fmt.Printf("足し算: %d\n", a+b)
    fmt.Printf("引き算: %d\n", a-b)
    fmt.Printf("掛け算: %d\n", a*b)
    fmt.Printf("割り算(商): %d\n", a/b)
    fmt.Printf("割り算(余り): %d\n", a%b)
}

出力:

a = 17, b = 5
足し算: 22
引き算: 12
掛け算: 85
割り算(商): 3
割り算(余り): 2

計算の確認:

  • 17 + 5 = 22
  • 17 - 5 = 12
  • 17 × 5 = 85
  • 17 ÷ 5 = 3余り2(商は3、余りは2)

---

課題3の解答

問題3-1: 買い物の計算

package main

import "fmt"

func main() {
    // 単価
    notebookPrice := 200
    penPrice := 150
    eraserPrice := 100

    // 個数
    notebookCount := 3
    penCount := 2
    eraserCount := 1

    // 小計
    notebookTotal := notebookPrice * notebookCount
    penTotal := penPrice * penCount
    eraserTotal := eraserPrice * eraserCount

    // 合計
    total := notebookTotal + penTotal + eraserTotal

    // 表示
    fmt.Println("===== レシート =====")
    fmt.Printf("ノート    %d円 × %d = %d円\n", notebookPrice, notebookCount, notebookTotal)
    fmt.Printf("ペン      %d円 × %d = %d円\n", penPrice, penCount, penTotal)
    fmt.Printf("消しゴム  %d円 × %d = %d円\n", eraserPrice, eraserCount, eraserTotal)
    fmt.Println("--------------------")
    fmt.Printf("合計: %d円\n", total)
}

出力:

===== レシート =====
ノート    200円 × 3 = 600円
ペン      150円 × 2 = 300円
消しゴム  100円 × 1 = 100円
--------------------
合計: 1000円

---

問題3-2: 温度変換

package main

import "fmt"

func main() {
    // 摂氏0度
    celsius1 := 0.0
    fahrenheit1 := celsius1*9/5 + 32
    fmt.Printf("摂氏 %.0f度 → 華氏 %.0f度\n", celsius1, fahrenheit1)

    // 摂氏25度
    celsius2 := 25.0
    fahrenheit2 := celsius2*9/5 + 32
    fmt.Printf("摂氏 %.0f度 → 華氏 %.0f度\n", celsius2, fahrenheit2)

    // 摂氏100度
    celsius3 := 100.0
    fahrenheit3 := celsius3*9/5 + 32
    fmt.Printf("摂氏 %.0f度 → 華氏 %.0f度\n", celsius3, fahrenheit3)
}

出力:

摂氏 0度 → 華氏 32度
摂氏 25度 → 華氏 77度
摂氏 100度 → 華氏 212度

計算の確認:

  • 0 × 9/5 + 32 = 32
  • 25 × 9/5 + 32 = 45 + 32 = 77
  • 100 × 9/5 + 32 = 180 + 32 = 212

---

問題3-3: BMI計算機

package main

import "fmt"

func main() {
    heightCm := 170.0  // 身長(cm)
    weight := 65.0     // 体重(kg)

    // cmをmに変換
    heightM := heightCm / 100.0

    // BMI計算
    bmi := weight / (heightM * heightM)

    fmt.Printf("身長: %.0fcm\n", heightCm)
    fmt.Printf("体重: %.0fkg\n", weight)
    fmt.Printf("BMI: %.2f\n", bmi)
}

出力:

身長: 170cm
体重: 65kg
BMI: 22.49

計算の確認:

  • 身長: 170cm = 1.70m
  • BMI = 65 ÷ (1.70 × 1.70) = 65 ÷ 2.89 ≈ 22.49

---

代替アプローチとトレードオフ分析

アプローチ1: 明示的な型宣言 vs 型推論

問題3-2: 温度変換

アプローチA: 型推論(:=

package main

import "fmt"

func main() {
    // 型推論を使用
    celsius := 25.0
    fahrenheit := celsius*9/5 + 32
    fmt.Printf("摂氏 %.0f度 → 華氏 %.0f度\n", celsius, fahrenheit)
}

メリット:

  • コードが簡潔
  • 書きやすい
  • 型が明白な場合に適している

デメリット:

  • 整数との混在時に注意が必要
  • 意図しない型になることがある

アプローチB: 明示的な型宣言

package main

import "fmt"

func main() {
    // 明示的に型を宣言
    var celsius float64 = 25.0
    var fahrenheit float64 = celsius*9/5 + 32
    fmt.Printf("摂氏 %.0f度 → 華氏 %.0f度\n", celsius, fahrenheit)
}

メリット:

  • 意図が明確
  • チームメンバーが理解しやすい
  • 型が重要な場合に適している

デメリット:

  • 冗長になる
  • タイピング量が増える

トレードオフ分析:

観点 型推論 明示的型宣言
可読性 高(簡潔) 高(明示的)
記述量 少ない 多い
型の明確さ
エラー発見 コンパイル時 コンパイル時
推奨度 型が明白な場合 型が重要な場合

推奨事項:

  • シンプルな計算: 型推論を使用
  • 複雑な計算や重要な型: 明示的宣言
  • チーム規約に従う

---

アプローチ2: 計算ロジックの分離

問題3-3: BMI計算機

アプローチA: インライン計算(初心者向け)

package main

import "fmt"

func main() {
    heightCm := 170.0
    weight := 65.0

    // すべてインラインで計算
    heightM := heightCm / 100.0
    bmi := weight / (heightM * heightM)

    fmt.Printf("身長: %.0fcm\n", heightCm)
    fmt.Printf("体重: %.0fkg\n", weight)
    fmt.Printf("BMI: %.2f\n", bmi)
}

メリット:

  • シンプルで理解しやすい
  • 初心者に適している
  • デバッグが容易

デメリット:

  • 再利用性が低い
  • テストが困難
  • スケールしない

アプローチB: 関数分離(中級者向け)

package main

import "fmt"

// BMI計算を関数として分離
func calculateBMI(heightCm, weight float64) float64 {
    heightM := heightCm / 100.0
    return weight / (heightM * heightM)
}

func main() {
    heightCm := 170.0
    weight := 65.0

    bmi := calculateBMI(heightCm, weight)

    fmt.Printf("身長: %.0fcm\n", heightCm)
    fmt.Printf("体重: %.0fkg\n", weight)
    fmt.Printf("BMI: %.2f\n", bmi)
}

メリット:

  • 再利用可能
  • テストが容易
  • 関心の分離

デメリット:

  • やや複雑
  • オーバーエンジニアリングの可能性

アプローチC: バリデーション付き(上級者向け)

package main

import (
    "fmt"
    "errors"
)

// エラーチェック付きBMI計算
func calculateBMI(heightCm, weight float64) (float64, error) {
    if heightCm <= 0 {
        return 0, errors.New("身長は正の値である必要があります")
    }
    if weight <= 0 {
        return 0, errors.New("体重は正の値である必要があります")
    }

    heightM := heightCm / 100.0
    bmi := weight / (heightM * heightM)
    return bmi, nil
}

func main() {
    heightCm := 170.0
    weight := 65.0

    bmi, err := calculateBMI(heightCm, weight)
    if err != nil {
        fmt.Printf("エラー: %v\n", err)
        return
    }

    fmt.Printf("身長: %.0fcm\n", heightCm)
    fmt.Printf("体重: %.0fkg\n", weight)
    fmt.Printf("BMI: %.2f\n", bmi)
}

メリット:

  • 堅牢性が高い
  • プロダクションレベル
  • エラー処理が適切

デメリット:

  • 複雑
  • 初心者には難しい

---

最適化パス:ナイーブな解法から最適化版へ

ケーススタディ: 複数の買い物計算

ステップ1: ナイーブな実装

package main

import "fmt"

func main() {
    // 商品1
    apple1 := 100
    apple2 := 100
    apple3 := 100
    totalApple := apple1 + apple2 + apple3

    // 商品2
    orange1 := 150
    orange2 := 150
    totalOrange := orange1 + orange2

    total := totalApple + totalOrange
    fmt.Printf("合計: %d円\n", total)
}

問題点:

  • 重複コードが多い
  • スケールしない
  • 保守性が低い

ステップ2: 変数の統合

package main

import "fmt"

func main() {
    // 単価を変数化
    applePrice := 100
    orangePrice := 150

    // 個数を変数化
    appleCount := 3
    orangeCount := 2

    // 小計を計算
    totalApple := applePrice * appleCount
    totalOrange := orangePrice * orangeCount

    total := totalApple + totalOrange
    fmt.Printf("合計: %d円\n", total)
}

改善点:

  • DRY原則(Don't Repeat Yourself)を適用
  • 価格変更が容易
  • 可読性向上

ステップ3: 構造化(Day 5で学ぶ)

package main

import "fmt"

type Item struct {
    name  string
    price int
    count int
}

func (i Item) subtotal() int {
    return i.price * i.count
}

func main() {
    items := []Item{
        {"りんご", 100, 3},
        {"みかん", 150, 2},
    }

    total := 0
    for _, item := range items {
        subtotal := item.subtotal()
        fmt.Printf("%s: %d円\n", item.name, subtotal)
        total += subtotal
    }

    fmt.Printf("合計: %d円\n", total)
}

最終改善点:

  • スケーラブル
  • 保守性が高い
  • プロダクションレベル

---

よくある間違いと修正

間違い1: 再宣言エラー

// NG: 同じ変数を2回宣言
score := 80
score := 90  // エラー: no new variables on left side of :=

// OK: 再代入は=を使う
score := 80
score = 90   // 正しい

エラーメッセージの読み方:

./main.go:10:2: no new variables on left side of :=

  • ./main.go:10:2: ファイル名、行番号、列番号
  • no new variables: 新しい変数がない
  • :=: 短縮宣言は新しい変数の作成専用

修正方法:

  • 2回目以降は=を使用
  • または、新しい変数と一緒に宣言(最低1つは新規変数が必要)
  • score := 80
    score, rank := 90, "A"  // rank が新規変数なのでOK
    

    ---

    間違い2: 型の不一致

    // NG: stringとintは結合できない
    age := 25
    fmt.Println("年齢は" + age + "歳")
    // エラー: invalid operation: "年齢は" + age (mismatched types string and int)
    
    // OK方法1: Printf を使用
    age := 25
    fmt.Printf("年齢は%d歳\n", age)
    
    // OK方法2: Sprintf で文字列変換
    age := 25
    text := fmt.Sprintf("年齢は%d歳", age)
    fmt.Println(text)
    
    // OK方法3: strconv.Itoa で変換
    import "strconv"
    age := 25
    fmt.Println("年齢は" + strconv.Itoa(age) + "歳")
    

    トレードオフ:

    方法 可読性 パフォーマンス 柔軟性
    Printf 最速
    Sprintf 中速
    strconv.Itoa 低速

    ---

    間違い3: 整数割り算の罠

    // 期待と異なる結果になる例
    result := 10 / 3   // result = 3(3.333...ではない)
    
    // 小数を得たい場合: 少なくとも1つをfloat64に
    result := 10.0 / 3.0  // result = 3.333...
    

    詳細な解説:

    // すべて整数 → 整数割り算
    a := 10 / 3        // 3
    b := 7 / 2         // 3
    
    // 少なくとも1つがfloat64 → 浮動小数点割り算
    c := 10.0 / 3      // 3.333...
    d := 10 / 3.0      // 3.333...
    e := float64(10) / 3  // 3.333...(型変換)
    
    // 丸めの動作
    f := 10 / 3        // 3(切り捨て)
    g := -10 / 3       // -3(ゼロ方向に丸める)
    

    実用例:

    // パーセンテージ計算の罠
    correctAnswers := 7
    totalQuestions := 10
    
    // NG: 整数割り算
    percentage := correctAnswers / totalQuestions * 100  // 0 (7/10=0)
    
    // OK: float64に変換
    percentage := float64(correctAnswers) / float64(totalQuestions) * 100  // 70.0
    

    ---

    間違い4: 変数のスコープ

    // NG: if文の中で宣言した変数は外で使えない
    if true {
        message := "Hello"
    }
    fmt.Println(message)  // エラー: undefined: message
    
    // OK: 外で宣言
    var message string
    if true {
        message = "Hello"
    }
    fmt.Println(message)  // "Hello"
    

    ---

    間違い5: ゼロ値の誤解

    // NG: 宣言だけでは値はゼロ値
    var score int
    // score = 0 (nilではない)
    
    if score == nil {  // エラー: invalid operation
        // ...
    }
    
    // OK: ゼロ値との比較
    if score == 0 {
        // ...
    }
    

    各型のゼロ値:

    ゼロ値 確認方法
    int 0 `if x == 0`
    float64 0.0 `if x == 0.0`
    string `""` `if x == ""` または `if len(x) == 0`
    bool false `if !x`
    ポインタ nil `if x == nil`

    ---

    ベンチマークとパフォーマンス分析

    文字列連結のパフォーマンス

    package main
    
    import (
        "fmt"
        "strings"
        "testing"
    )
    
    // ベンチマーク1: + 演算子
    func BenchmarkConcatPlus(b *testing.B) {
        for i := 0; i < b.N; i++ {
            result := ""
            for j := 0; j < 100; j++ {
                result += "hello"
            }
        }
    }
    
    // ベンチマーク2: fmt.Sprintf
    func BenchmarkConcatSprintf(b *testing.B) {
        for i := 0; i < b.N; i++ {
            result := ""
            for j := 0; j < 100; j++ {
                result = fmt.Sprintf("%s%s", result, "hello")
            }
        }
    }
    
    // ベンチマーク3: strings.Builder(最速)
    func BenchmarkConcatBuilder(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var builder strings.Builder
            for j := 0; j < 100; j++ {
                builder.WriteString("hello")
            }
            _ = builder.String()
        }
    }
    

    実行結果:

    BenchmarkConcatPlus-8       10000   150000 ns/op   500000 B/op   100 allocs/op
    BenchmarkConcatSprintf-8     8000   180000 ns/op   600000 B/op   200 allocs/op
    BenchmarkConcatBuilder-8   100000    15000 ns/op    50000 B/op     5 allocs/op
    

    分析:

    方法 速度 メモリ 割り当て回数 推奨用途
    + 演算子 遅い 多い 多い 短い文字列
    Sprintf 最も遅い 最も多い 最も多い フォーマットが必要な場合
    Builder 最速 最小 最小 ループ内の連結

    実用的な推奨:

    // 1-2個の文字列連結: + 演算子(可読性優先)
    fullName := firstName + " " + lastName
    
    // フォーマットが必要: Sprintf
    message := fmt.Sprintf("ユーザー%sは%d歳です", name, age)
    
    // ループ内や多数の連結: strings.Builder
    var builder strings.Builder
    for _, word := range words {
        builder.WriteString(word)
        builder.WriteString(" ")
    }
    result := builder.String()
    

    ---

    変数割り当てのパフォーマンス

    package main
    
    import "testing"
    
    // ベンチマーク1: 都度割り当て
    func BenchmarkAllocateEachTime(b *testing.B) {
        for i := 0; i < b.N; i++ {
            data := make([]int, 1000)
            _ = data
        }
    }
    
    // ベンチマーク2: 事前割り当て
    func BenchmarkPreAllocate(b *testing.B) {
        data := make([]int, 1000)
        for i := 0; i < b.N; i++ {
            _ = data
        }
    }
    
    // ベンチマーク3: プール使用(上級)
    var pool = make([]int, 1000)
    
    func BenchmarkUsePool(b *testing.B) {
        for i := 0; i < b.N; i++ {
            data := pool
            _ = data
        }
    }
    

    実行結果:

    BenchmarkAllocateEachTime-8   1000000   1200 ns/op   8192 B/op   1 allocs/op
    BenchmarkPreAllocate-8       100000000    10 ns/op      0 B/op   0 allocs/op
    BenchmarkUsePool-8           100000000     2 ns/op      0 B/op   0 allocs/op
    

    パフォーマンス改善のヒント:

  • ループ外で割り当て: 可能な限りループの外で変数を宣言
  • 容量を事前指定: スライス、マップの容量を事前に指定
  • 不要な変数を避ける: 中間変数を最小化
  • 定数を使用: 変更しない値は定数化
  • ---

    実践的なコーディングパターン

    パターン1: 設定値の管理

    package main
    
    import "fmt"
    
    // 定数で設定値を管理(変更されない値)
    const (
        MaxRetries = 3
        Timeout    = 30  // 秒
        BufferSize = 1024
    )
    
    // 変数で実行時設定を管理(変更される可能性がある値)
    var (
        Debug      = false
        LogLevel   = "info"
        ServerPort = 8080
    )
    
    func main() {
        fmt.Printf("最大リトライ回数: %d\n", MaxRetries)
        fmt.Printf("タイムアウト: %d秒\n", Timeout)
        fmt.Printf("デバッグモード: %v\n", Debug)
    }
    

    パターン2: エラーハンドリングと変数

    package main
    
    import (
        "fmt"
        "strconv"
    )
    
    func main() {
        // 文字列を整数に変換(エラーの可能性あり)
        input := "123"
    
        // 変換とエラーチェック
        num, err := strconv.Atoi(input)
        if err != nil {
            fmt.Printf("変換エラー: %v\n", err)
            return
        }
    
        fmt.Printf("変換成功: %d\n", num)
    }
    

    パターン3: 複数戻り値の活用

    package main
    
    import "fmt"
    
    // 計算結果とエラーを返す
    func divide(a, b int) (int, error) {
        if b == 0 {
            return 0, fmt.Errorf("ゼロ除算エラー")
        }
        return a / b, nil
    }
    
    func main() {
        result, err := divide(10, 2)
        if err != nil {
            fmt.Printf("エラー: %v\n", err)
            return
        }
        fmt.Printf("結果: %d\n", result)
    }
    

    ---

    まとめ:ベストプラクティス

    変数宣言のガイドライン

  • := を優先: 型が明白な場合は型推論を使用
  • var を使用: 型が重要、またはゼロ値初期化が必要な場合
  • const を使用: 変更しない値は定数化
  • グローバル変数を避ける: パッケージレベル変数は最小限に
  • 命名のベストプラクティス

    // Good
    userCount := 10
    isActive := true
    maxRetries := 3
    totalPrice := calculatePrice()
    
    // Bad
    x := 10
    flag := true
    num := 3
    data := calculatePrice()
    

    パフォーマンスのヒント

  • ループ外で変数を宣言
  • 不要な型変換を避ける
  • 文字列連結はstrings.Builderを検討
  • スライス/マップは容量を事前指定