Day 1: Go言語の核心 - 解説

学習目標

このDay 1では、他言語経験者がGoの本質を理解するための核心概念を学びます:

  • 型システムの哲学: なぜGoは暗黙の変換を禁止するのか
  • メモリモデルの明示性: 値とポインタの使い分け
  • スライスの内部構造: 参照型の仕組みとメモリ効率
  • ゼロ値の設計: 初期化不要で使える型の設計哲学
  • 本日のまとめ

    今日学んだGo言語の核心:

    概念 説明 重要度 他言語との違い
    設計思想 シンプルさと効率性の追求 ★★★ 機能を削ぎ落とす哲学
    型システム 静的型付け、暗黙の変換なし ★★★ 型安全性を最優先
    ゼロ値 すべての変数は初期化される ★★★ nil/undefinedがない
    ポインタ 明示的だが安全なメモリアクセス ★★★ ポインタ演算禁止
    参照型 スライス、マップ、チャネル ★★☆ 内部構造が明確
    エラー処理 例外ではなく値として扱う ★★★ try-catchなし

    ---

    Goの設計哲学: なぜこの言語が生まれたのか

    背景: Googleが抱えていた問題

    2007年、Googleのエンジニアは次の問題に直面していました:

  • C++のビルド時間が長すぎる(数時間)
  • 動的言語(Python等)は開発は速いが実行が遅い
  • 並行処理が難しい(マルチコアCPUを活かせない)
  • コードの可読性が低い(言語機能が多すぎる)

Goの設計目標

目標 アプローチ 結果
高速なコンパイル 依存関係の明示化、シンプルな文法 数秒でビルド完了
実行速度 ネイティブコード生成、効率的なGC C/C++に近い速度
並行処理 goroutineとchannel 簡単に並行処理を記述
可読性 機能を最小限に、統一されたフォーマット 誰が書いても同じコード

他言語との比較マトリクス

複雑さ vs パフォーマンス

複雑さ
  ↑
  │                     C++
  │                      ●
  │
  │              Java
  │               ●
  │
  │          Go  ●
  │
  │                 Python
  │                   ●
  │         JavaScript
  │            ●
  │
  └─────────────────────────→ パフォーマンス
     遅い              速い

Goは「シンプルさ」と「高パフォーマンス」の両立を目指しています。

Goの「引き算の哲学」

多くの言語が機能を追加していく中、Goは意図的に機能を削ぎ落としました:

機能 他言語 Go 理由
関数オーバーロード × 名前の曖昧さを排除
演算子オーバーロード × 予期しない動作を防ぐ
継承 × コンポジションを推奨
例外 × エラーを明示的に
ジェネリクス △ (1.18以降) 必要最小限の実装
マクロ × コードの透明性

「少ないほど多い(Less is more)」- Rob Pike

---

メンタルモデル: Goプログラマーの思考法

1. 明示性を重視する

他言語(暗黙的):

# Python - 多くのことが暗黙的に行われる
def process(data):
    return data * 2  # int/str/list、何でも動く

result = process(5)       # 10
result = process("Hi")    # "HiHi"
result = process([1, 2])  # [1, 2, 1, 2]

Go(明示的):

// Go - 型を明示、意図を明確に
func processInt(data int) int {
    return data * 2  // intのみ
}

func processString(data string) string {
    return data + data  // stringのみ
}

// 使用時に型が明確
result1 := processInt(5)        // int
result2 := processString("Hi")  // string

メンタルモデル:

  • 「動くかもしれない」より「間違いなく動く」
  • 型エラーは開発時に検出したい(ランタイムではなく)
  • コードを読む人(未来の自分を含む)のために明示する
  • IDEの補完が正確に効く

2. コピーか参照かを意識する

他言語(隠蔽):

# Python - 実装の詳細が隠されている
def modify(lst):
    lst.append(1)  # 元のリストが変わる

def modify_num(n):
    n = n + 1  # 元の数値は変わらない

my_list = []
modify(my_list)  # [1] - 変わる

my_num = 5
modify_num(my_num)  # 5 - 変わらない

Go(明示):

// Go - コピーか参照かを明示的に選ぶ
func modifySlice(s []int) []int {
    s = append(s, 1)
    return s  // appendは新しいスライスを返す可能性がある
}

func modifySlicePointer(s *[]int) {
    *s = append(*s, 1)  // ポインタ経由で確実に変更
}

// 構造体の場合
type User struct {
    Name string
    Age  int
}

func modifyByValue(u User) User {
    u.Age++
    return u  // コピーを返す
}

func modifyByPointer(u *User) {
    u.Age++  // 元を変更
}

メンタルモデル:

  • 小さい値(int, float, 小さいstruct)→ 値渡し(コピー)
  • 大きい値、変更が必要 → ポインタ渡し(参照)
  • スライス、マップ → 既に参照的な性質を持つ(内部でポインタを使用)
  • 関数シグネチャを見れば、変更されるかどうかわかる

3. ゼロ値で有効な設計

他言語(初期化必須):

// Java - 必ず初期化が必要
List<Integer> list = new ArrayList<>();  // 初期化必須
Map<String, Integer> map = new HashMap<>();  // 初期化必須

// 初期化を忘れるとNullPointerException
String s;
System.out.println(s.length());  // エラー!

Go(ゼロ値で有効):

// Go - ゼロ値で使える設計
var mu sync.Mutex  // 初期化不要
mu.Lock()
mu.Unlock()

var buf bytes.Buffer  // 初期化不要
buf.WriteString("hello")
fmt.Println(buf.String())

var wg sync.WaitGroup  // 初期化不要
wg.Add(1)
go func() {
    defer wg.Done()
    // ...
}()
wg.Wait()

メンタルモデル:

  • 型を設計する際、ゼロ値で有効にできないか考える
  • make()new() が不要なら、より使いやすいAPI
  • 初期化を忘れるバグを防ぐ
  • シンプルなAPIを提供する

ゼロ値の一覧:

var i int        // 0
var f float64    // 0.0
var b bool       // false
var s string     // ""
var p *int       // nil
var sl []int     // nil (でもappendは使える)
var m map[string]int  // nil (読み込みはOK、書き込みはNG)
var fn func()    // nil

4. エラーは値として扱う

他言語(例外):

# Python - try-catchで制御フロー
try:
    file = open("data.txt")
    data = file.read()
except FileNotFoundError:
    data = ""
except IOError as e:
    log.error(f"IO error: {e}")
    data = ""
finally:
    if file:
        file.close()

// JavaScript - Promise/async-awaitでエラー処理
async function loadData() {
    try {
        const response = await fetch('data.json');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error(error);
        return null;
    }
}

Go(エラー値):

// Go - エラーを明示的にチェック
file, err := os.Open("data.txt")
if err != nil {
    // エラー処理
    log.Printf("failed to open file: %v", err)
    return
}
defer file.Close()  // 必ず実行される

data, err := io.ReadAll(file)
if err != nil {
    log.Printf("failed to read file: %v", err)
    return
}

// dataを使用
fmt.Println(string(data))

メンタルモデル:

  • エラーは例外的な状況ではなく、通常の結果の一つ
  • エラー処理を強制することで、堅牢なコードを書く
  • 制御フローが追いやすい(例外で飛ばない)
  • エラーハンドリングがコードの一部として可視化される

---

詳細なアルゴリズム分析

1. 型システムとパフォーマンス

型チェックのコスト比較

動的型付け言語(Python):

def add(a, b):
    return a + b

# 内部で実行される処理:
# 1. a の型オブジェクトを取得(メモリアクセス)
# 2. b の型オブジェクトを取得(メモリアクセス)
# 3. a.__add__ メソッドを探索(辞書検索)
# 4. 型が一致するかチェック
# 5. 適切な加算関数を呼び出し(関数呼び出しオーバーヘッド)

# 合計: 約100-200ns(実装による)

静的型付け(Go):

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

// コンパイル時に生成されるアセンブリ:
// MOV AX, [a]    ; a をレジスタに読み込み
// ADD AX, [b]    ; b を加算
// RET            ; 結果を返す

// 合計: 約0.3-0.5ns

パフォーマンス比較実測:

Python (CPython 3.11):  約150ns/op
JavaScript (V8):        約 10ns/op (JIT最適化後)
Java (HotSpot):         約  2ns/op (JIT最適化後)
Go:                     約 0.5ns/op
C:                      約 0.3ns/op

結論: Goの型システムはC並みのパフォーマンス

ジェネリクスの実装戦略

Go 1.18以降のジェネリクスは「GCShape Stenciling」という手法を使用:

// ジェネリクス関数
func Add[T int | int64 | float64](a, b T) T {
    return a + b
}

// コンパイラの処理:
// 1. 型パラメータを解析
// 2. 使用されている具体的な型を特定
// 3. 型ごとに専用コードを生成(モノモーフィゼーション)

// 生成される関数(簡略化):
func Add_int(a, b int) int { return a + b }
func Add_int64(a, b int64) int64 { return a + b }
func Add_float64(a, b float64) float64 { return a + b }

GCShape Stenciling:

  • 同じメモリレイアウトの型は同じコードを共有
  • 例: int32とfloat32は同じ4バイト → 同じコード
  • バイナリサイズとパフォーマンスのバランス

他言語との比較:

言語 ジェネリクス手法 パフォーマンス バイナリサイズ
C++ テンプレート(完全特殊化) 最速 大きい
Java 型消去(Type Erasure) 遅い(キャスト必要) 小さい
Go GCShape Stenciling 速い 中程度
Rust モノモーフィゼーション 最速 大きい

2. スライスの成長アルゴリズム詳細

Go 1.18以降の成長戦略(実装)

// runtime/slice.go からの抜粋(簡略化)
func growslice(et *_type, old slice, cap int) slice {
    newcap := old.cap
    doublecap := newcap + newcap

    if cap > doublecap {
        // 必要な容量が倍より大きい場合、その容量を使う
        newcap = cap
    } else {
        const threshold = 256
        if old.cap < threshold {
            // 小さいスライスは倍々で成長
            newcap = doublecap
        } else {
            // 大きいスライスは約1.25倍で成長
            for 0 < newcap && newcap < cap {
                // 成長式: new = old + (old + 3*256) / 4
                newcap += (newcap + 3*threshold) / 4
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    // メモリアライメントとサイズクラスの調整
    // mallocgcの割り当てサイズに合わせて最適化
    var overflow bool
    var lenmem, newlenmem, capmem uintptr

    switch {
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        overflow = uintptr(newcap) > maxAlloc
        newcap = int(capmem)
    case et.size == sys.PtrSize:
        lenmem = uintptr(old.len) * sys.PtrSize
        newlenmem = uintptr(cap) * sys.PtrSize
        capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
        newcap = int(capmem / sys.PtrSize)
    // その他のサイズ...
    }

    // 新しい配列を割り当て、古いデータをコピー
    var p unsafe.Pointer
    if et.ptrdata == 0 {
        p = mallocgc(capmem, nil, false)
        memmove(p, old.array, lenmem)
    } else {
        p = mallocgc(capmem, et, true)
        if writeBarrier.enabled {
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
        }
        memmove(p, old.array, lenmem)
    }

    return slice{p, old.len, newcap}
}

成長パターンの実測

初期容量: 1

append回数  容量    成長率
    1       1       -
    2       2       2.00x
    3       4       2.00x
    5       8       2.00x
    9      16       2.00x
   17      32       2.00x
   33      64       2.00x
   65     128       2.00x
  129     256       2.00x
  257     512       2.00x  ← ここから変化
  513     848       1.66x
  849    1280       1.51x
 1281    1792       1.40x
 1793    2560       1.43x

なぜ256で戦略が変わるのか?:

  • 小さいスライス: 再割り当て回数を減らす(2倍で急成長)
  • 大きいスライス: メモリ浪費を防ぐ(1.25倍で緩やか)
  • 256という閾値: 経験的に決定された最適値

メモリ使用量とパフォーマンス

例: 1000要素を追加する3つのパターン

【パターンA: 事前割り当てなし】
var s []int
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

結果:
- 再割り当て: 10回
- ピークメモリ: 約16KB
- 無駄なメモリ: 約8KB (容量1024、使用1000)
- 実行時間: 約25μs

【パターンB: 事前に容量確保】
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

結果:
- 再割り当て: 0回
- ピークメモリ: 8KB
- 無駄なメモリ: 0KB
- 実行時間: 約5μs (5倍高速!)

【パターンC: 長さも指定】
s := make([]int, 1000)
for i := 0; i < 1000; i++ {
    s[i] = i
}

結果:
- 再割り当て: 0回
- ピークメモリ: 8KB
- 無駄なメモリ: 0KB
- 実行時間: 約3μs (8倍高速!)

3. ポインタとエスケープ分析

エスケープ分析の仕組み

Goコンパイラは静的解析で変数の生存期間を判定:

// ケース1: スタックに割り当て
func localVar() int {
    x := 42  // x は関数内でのみ使用
    y := x * 2
    return y  // 値のみ返す → スタック
}

// 生成されるコード:
// スタックフレーム内で完結、ヒープ割り当てなし

// ケース2: ヒープにエスケープ
func escapeVar() *int {
    x := 42  // x のポインタが返される
    return &x  // → ヒープに割り当て必要
}

// 生成されるコード:
// runtime.newobject() でヒープに割り当て

エスケープ分析の確認方法:

# エスケープ分析の詳細を表示
go build -gcflags="-m -m" main.go

# 出力例:
# ./main.go:6:2: x escapes to heap:
# ./main.go:6:2:   flow: ~r0 = &x:
# ./main.go:6:2:     from &x (address-of) at ./main.go:7:9
# ./main.go:6:2:     from return &x (return) at ./main.go:7:2

スタック vs ヒープのパフォーマンス

スタック割り当て:
- 確保: SP(スタックポインタ)を動かすだけ(1命令)
- 解放: 関数終了時に自動(SP を戻すだけ)
- メモリ局所性: 高い(キャッシュに乗りやすい)
- GCの負荷: ゼロ
- コスト: 約0.5-1ns

ヒープ割り当て:
- 確保: mallocgc() 呼び出し(複雑な処理)
- 解放: GCが必要(スキャン、マーク、スイープ)
- メモリ局所性: 低い(散在する可能性)
- GCの負荷: あり(GC pause の原因)
- コスト: 約50-100ns

結論: スタック割り当ては100倍以上高速

エスケープを避けるテクニック

// 悪い例: 不要なポインタ返却
func getConfig() *Config {
    return &Config{  // ヒープにエスケープ
        Timeout: 30,
        MaxConn: 100,
    }
}

// 良い例: 値で返す
func getConfig() Config {
    return Config{  // スタックで完結
        Timeout: 30,
        MaxConn: 100,
    }
}

// 悪い例: クロージャでの変数キャプチャ
func makeCounter() func() int {
    count := 0  // エスケープ(クロージャが参照)
    return func() int {
        count++
        return count
    }
}

// 良い例: ポインタレシーバーで明示的に管理
type Counter struct {
    count int
}

func (c *Counter) Increment() int {
    c.count++
    return c.count
}

---

ビジュアル図解: メモリレイアウトとデータフロー

1. スライスの内部構造

スライスの定義:
s := []int{10, 20, 30, 40, 50}

メモリレイアウト:

【スライスヘッダ】(スタック上、24バイト)
┌──────────────┬──────┬──────┐
│ ptr (8 bytes)│ len  │ cap  │
│ 0x00400000   │  5   │  5   │
└──────┬───────┴──────┴──────┘
       │
       │ ポインタが指す先
       ↓
【配列データ】(ヒープ上、40バイト)
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │ (各8バイト)
└────┴────┴────┴────┴────┘
0x400000  408   410   418   420

部分スライスの作成:
sub := s[1:4]

【新しいスライスヘッダ】(スタック上)
┌──────────────┬──────┬──────┐
│ ptr (8 bytes)│ len  │ cap  │
│ 0x00400008   │  3   │  4   │
└──────┬───────┴──────┴──────┘
       │
       │ 同じ配列の別の位置を指す
       ↓
【共有される配列データ】
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴─┬──┴────┴────┴────┘
       └─ sub の開始位置

重要: 部分スライスは元の配列を共有する!

2. append時の再割り当て

初期状態:
s := make([]int, 3, 5)
s[0], s[1], s[2] = 1, 2, 3

【スライス s】
┌──────┬─────┬─────┐
│ ptr  │ len │ cap │
│0x1000│  3  │  5  │
└───┬──┴─────┴─────┘
    │
    ↓
【配列】(容量5)
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 0 │ 0 │ ← 未使用領域
└───┴───┴───┴───┴───┘

容量内でappend:
s = append(s, 4)

【スライス s】(ポインタは変わらない)
┌──────┬─────┬─────┐
│ ptr  │ len │ cap │
│0x1000│  4  │  5  │
└───┬──┴─────┴─────┘
    │
    ↓
【配列】(同じ配列)
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 0 │
└───┴───┴───┴───┴───┘

容量を超えるappend:
s = append(s, 5, 6)

【新しいスライス s】
┌──────┬─────┬─────┐
│ ptr  │ len │ cap │
│0x2000│  6  │ 10  │ ← 新しいアドレス、容量2倍
└───┬──┴─────┴─────┘
    │
    ↓
【新しい配列】(容量10)
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 0 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

【古い配列】(GCの対象)
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ ← もう使われていない
└───┴───┴───┴───┴───┘

3. 値渡しとポインタ渡しの違い

構造体の定義:
type User struct {
    Name string  // 16 bytes (stringヘッダ)
    Age  int     //  8 bytes
}

値渡し:
func updateAge(u User) {
    u.Age++  // コピーを変更
}

【呼び出し元】           【関数内】
main()                  updateAge()
┌────────────┐          ┌────────────┐
│ user       │          │ u (copy)   │
│ Name:"Alice" ────コピー───→ Name:"Alice"│
│ Age: 25    │          │ Age: 26    │ ← 変更
└────────────┘          └────────────┘
    ↑                        ↓
    └─── 変更されない          GCされる

ポインタ渡し:
func updateAge(u *User) {
    u.Age++  // 元を変更
}

【呼び出し元】           【関数内】
main()                  updateAge()
┌────────────┐          ┌────────┐
│ user       │←─ポインタ─│ u (*User)│
│ Name:"Alice"│          │0x1234  │
│ Age: 26    │ ← 変更    └────────┘
└────────────┘
0x1234

コピーされるのはポインタ(8バイト)のみ

4. インターフェース値の内部構造

インターフェースの定義:
type Writer interface {
    Write([]byte) (int, error)
}

具体的な型:
type FileWriter struct {
    path string
}

func (f *FileWriter) Write(data []byte) (int, error) {
    // ...
}

インターフェース値の作成:
var w Writer = &FileWriter{path: "file.txt"}

【インターフェース値 w】(16バイト)
┌──────────┬──────────┐
│   tab    │   data   │
│ (8 bytes)│ (8 bytes)│
└────┬─────┴────┬─────┘
     │          │
     │          └─→ 【FileWriter のインスタンス】
     │              ┌──────────────┐
     │              │ path: "file.txt"│
     │              └──────────────┘
     │              0x2000
     │
     └─→ 【itab (interface table)】
         ┌─────────────────┐
         │ inter: *Writer  │ ← インターフェース型情報
         │ _type: *FileWriter│ ← 実際の型情報
         │ fun[0]: Write   │ ← メソッドポインタ
         └─────────────────┘

メソッド呼び出し:
w.Write(data)

実際の処理:
1. itab から Write メソッドのポインタを取得
2. data ポインタを引数として渡す
3. 間接呼び出し(仮想関数テーブル方式)

コスト: 直接呼び出しより約2-3ns遅い

---

さらなる探求: 学習リソース

公式ドキュメント

- インタラクティブなチュートリアル - ブラウザで実行可能 - 初心者から経験者まで必須

- Goらしいコードの書き方 - イディオム集 - 標準ライブラリの設計思想

- 公式ブログ - 言語の設計判断の背景 - 新機能の詳細解説

- 言語仕様の正式ドキュメント - 曖昧な挙動の確認に

推奨書籍

  • "The Go Programming Language" (Donovan & Kernighan)
- 通称「ゴーファーバイブル」 - 体系的な学習に最適 - サンプルコードが豊富

  • "Go in Action" (William Kennedy)
- 実践的な内容 - 並行処理の解説が詳しい

  • "Learning Go" (Jon Bodner)
- 2021年出版の新しい本 - モダンなGoの書き方

オンラインリソース

- 実例ベースの学習 - コピー&ペーストで試せる

- 週次のニュースレター - 最新情報をキャッチアップ

- パッケージのドキュメント検索 - 使用例とサンプルコード

- ブラウザでGoを実行 - コードの共有に便利

コミュニティ

- 活発なコミュニティ - 質問と回答

- Redditのコミュニティ - ニュースと議論

- 公式フォーラム - 詳しい議論

---

セルフチェック質問

これらの質問に答えられるか確認してください。答えられない場合は、該当セクションを復習しましょう。

レベル1: 基礎理解

  • 型システム
- Goで intint32 を加算するとどうなりますか? - なぜGoは暗黙の型変換を禁止しているのですか? - ジェネリクス(Go 1.18+)を使う利点は何ですか?

  • ゼロ値
- 次の変数のゼロ値は何ですか:int, string, []int, map[string]int, *int - なぜGoはすべての変数をゼロ値で初期化するのですか? - ゼロ値で使える型を設計する際のポイントは?

  • ポインタ
- 値渡しとポインタ渡しの違いは何ですか? - いつポインタレシーバーを使うべきですか? - エスケープ分析とは何ですか?

レベル2: 実践的理解

  • スライス
- スライスの内部構造(ptr, len, cap)を説明できますか? - append() は新しいスライスを返す場合と返さない場合があります。その違いは? - 次のコードの出力は何ですか?
     s := []int{1, 2, 3}
     sub := s[1:]
     sub[0] = 99
     fmt.Println(s)  // 何が出力される?
     

  • メモリ管理
- スタック割り当てとヒープ割り当ての違いは? - 次のコードでエスケープが発生するのはどれですか?
     func f1() int { x := 42; return x }
     func f2() *int { x := 42; return &x }
     

  • パフォーマンス
- 事前に容量を確保したスライス(make([]int, 0, 1000))はなぜ速いのですか? - 小さな構造体(16バイト以下)はポインタ渡しと値渡しのどちらが速いですか?

レベル3: 設計思想の理解

  • Goの哲学
- 「Less is more」はGoのどの設計判断に現れていますか? - Goが関数オーバーロードを提供しない理由は? - Goが例外ではなくエラー値を使う理由は?

  • メンタルモデル
- Goプログラマーが「明示性」を重視する理由は? - インターフェースの内部構造(itab, data)を説明できますか? - なぜGoは参照型(スライス、マップ)を特別扱いしているのですか?

  • トレードオフ
- 型安全性とパフォーマンスのトレードオフをGoはどう解決していますか? - ジェネリクスの「GCShape Stenciling」の利点と欠点は? - スライスの成長戦略(容量256以下で2倍、以上で1.25倍)の理由は?

解答のヒント

クリックして解答のヒントを表示

  • コンパイルエラー。明示的に int32(x) + y のように変換が必要。
  • バグの早期発見、意図の明確化、予期しない動作の防止。
  • ゼロ値: 0, "", nil, nil, nil。理由: 初期化忘れを防ぐ、安全性。
  • 値渡し=コピー、ポインタ渡し=参照。大きい構造体や変更が必要ならポインタ。
  • スライス: ヘッダ(ptr, len, cap)+ 実際の配列。
  • 容量を超えた場合は新しい配列を割り当て、新しいスライスを返す。
  • [1, 99, 3] - subはsと配列を共有しているため。
  • スタック=高速、関数終了で自動解放。ヒープ=GC必要、エスケープ時。
  • f2がエスケープ(ポインタを返すため)。
  • 再割り当てが不要なため。小さい構造体は値渡しの方が速い(レジスタに収まる)。

---

明日の予習: Day 2 - イディオマティックGo

Day 2では、Goらしいコードの書き方(イディオム)を学びます:

予定トピック

  • 命名規則とエクスポート
  • - パッケージ、変数、関数の命名ルール - 大文字/小文字でのエクスポート制御 - Getter/Setterパターン

    • エラーハンドリングのパターン
    - エラーのラップとerrors.Is, errors.As - カスタムエラー型の設計 - センチネルエラー vs エラー型

    • defer, panic, recover
    - deferの実行順序と活用例 - panicを使うべき場面 - recoverによるパニックからの回復

    • インターフェースの活用
    - 小さいインターフェースの設計(io.Reader, io.Writer) - インターフェース分離の原則 - 空インターフェースの使い方と注意点

    • テストの書き方
    - テーブル駆動テスト - テストヘルパーとサブテスト - ベンチマークとプロファイリング

    予習課題

    次のコードを読んで、何が問題か考えてみてください:

    // これは良いGoコード?それとも改善の余地がある?
    type MyInterface interface {
        DoEverything(a int, b string, c float64) (int, string, error)
        ProcessData(data []byte) error
        ValidateInput(input string) bool
        GetName() string
        SetName(name string)
        GetAge() int
        SetAge(age int)
    }
    
    func HandleError(err error) {
        if err != nil {
            panic(err)  // これは適切?
        }
    }
    
    func ReadFile(path string) string {
        data, _ := os.ReadFile(path)  // エラーを無視
        return string(data)
    }
    

    答えは明日!

    ---

    おめでとうございます!Day 1を完了しました。

    今日学んだGoの核心概念:

    • ✅ 型システムの明示性
    • ✅ ポインタとメモリ効率
    • ✅ スライスの内部構造
    • ✅ ゼロ値の設計哲学

    これらの基礎の上に、明日はGoらしいコードの書き方を学びます。

    復習のポイント:

    • 型変換は常に明示的に
    • 値渡しとポインタ渡しを意識する
    • スライスは容量を事前確保すると高速
    • ゼロ値で使える型を設計する

    それでは、明日もお楽しみに!