Day 1: Go言語の核心 - 解答例

チャレンジ1の解答: 型システムの理解

アプローチ1: 他言語からの移行者が最初に書くコード

package main

import "fmt"

// Python/JavaScriptなどの動的型付け言語からの移行者が
// 最初に試みるアプローチ - コンパイルエラーになる
/*
func add(a, b interface{}) interface{} {
    // これはGoでは推奨されない
    // interface{}は型安全性を失う
    return a.(int) + b.(int) // ランタイムパニックのリスク
}
*/

// Java/C++のオーバーロードに慣れた人が期待するもの
// しかしGoには関数オーバーロードがない!
func addInt(a, b int) int {
    return a + b
}

func addFloat64(a, b float64) float64 {
    return a + b
}

func addInt64(a, b int64) int64 {
    return a + b
}

func main() {
    // 明示的な型宣言
    var x int = 10
    var y int = 20
    fmt.Println("int:", addInt(x, y))

    // 型推論(:=)を使用
    a := 10.5
    b := 20.3
    fmt.Println("float64:", addFloat64(a, b))

    // 明示的なキャスト(Goでは必須)
    var i int = 10
    var f float64 = 3.14
    // result := i + f  // コンパイルエラー!
    result := float64(i) + f // 正しい方法
    fmt.Println("mixed:", result)

    // 型の異なる変数の演算には常にキャストが必要
    var int32Num int32 = 100
    var int64Num int64 = 200
    // sum := int32Num + int64Num  // コンパイルエラー!
    sum := int64(int32Num) + int64Num // 明示的変換
    fmt.Println("sum:", sum)
}

アプローチ2: Go 1.18以降のジェネリクス(推奨)

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// ジェネリクス関数 - 1つの実装で複数の型に対応
// constraints.Orderedは比較可能な型を表す
func Add[T constraints.Ordered](a, b T) T {
    return a + b
}

// カスタム型制約の定義
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

// より厳密な数値型のみの制約
func AddNumbers[T Number](a, b T) T {
    return a + b
}

// 複数の型パラメータを持つ関数
func Convert[T, U Number](value T) U {
    return U(value)
}

func main() {
    // 型推論が効く
    fmt.Println("int:", Add(10, 20))
    fmt.Println("float64:", Add(10.5, 20.3))
    fmt.Println("string:", Add("hello", " world"))

    // 明示的な型指定も可能
    result := Add[int64](1000000000, 2000000000)
    fmt.Println("int64:", result)

    // 型変換を伴うジェネリクス
    var x int32 = 100
    var y float64 = Convert[int32, float64](x)
    fmt.Println("converted:", y)
}

アプローチ3: インターフェースを使った多態性(Goらしい方法)

package main

import "fmt"

// 加算可能な型を表すインターフェース
type Addable interface {
    Add(other Addable) Addable
}

// Int型(intのラッパー)
type Int int

func (i Int) Add(other Addable) Addable {
    // 型アサーションで具体的な型を取得
    if o, ok := other.(Int); ok {
        return Int(int(i) + int(o))
    }
    panic("incompatible type")
}

// Float型(float64のラッパー)
type Float float64

func (f Float) Add(other Addable) Addable {
    if o, ok := other.(Float); ok {
        return Float(float64(f) + float64(o))
    }
    panic("incompatible type")
}

// ジェネリックな加算関数
func AddValues(a, b Addable) Addable {
    return a.Add(b)
}

func main() {
    x := Int(10)
    y := Int(20)
    fmt.Println("Int:", AddValues(x, y))

    a := Float(10.5)
    b := Float(20.3)
    fmt.Println("Float:", AddValues(a, b))
}

最適化パス: どのアプローチを選ぶべきか

アプローチ 利点 欠点 使用場面
個別関数 シンプル、読みやすい コード重複 型が少ない場合
ジェネリクス コード再利用、型安全 やや複雑 Go 1.18以降で汎用的な処理
インターフェース 多態性、拡張性 ボイラープレート OOP的な設計が必要な場合

よくある間違い

package main

import "fmt"

func demonstrateCommonMistakes() {
    // 間違い1: interface{}を乱用する(型安全性を失う)
    var x interface{} = 10
    var y interface{} = 20
    // z := x + y  // コンパイルエラー!interface{}は演算できない

    // 間違い2: 型アサーションをチェックせずに使う
    // result := x.(int) + y.(int)  // パニックのリスク

    // 正しい方法: カンマokイディオムで安全にチェック
    if xi, ok := x.(int); ok {
        if yi, ok := y.(int); ok {
            fmt.Println("safe addition:", xi+yi)
        }
    }

    // 間違い3: 異なる数値型を暗黙的に混ぜる(Python/JavaScriptの癖)
    var a int32 = 10
    var b int64 = 20
    // c := a + b  // コンパイルエラー!
    c := int64(a) + b // 明示的変換が必要
    fmt.Println("converted:", c)

    // 間違い4: floatとintを混ぜる
    var i int = 10
    var f float64 = 3.14
    // result := i + f  // コンパイルエラー!
    result := float64(i) + f
    fmt.Println("result:", result)
}

ベンチマーク比較

package main

import (
    "testing"
)

// 個別関数版
func addInt(a, b int) int {
    return a + b
}

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

// インターフェース版
type Addable interface {
    Add(other Addable) Addable
}
type Int int
func (i Int) Add(other Addable) Addable {
    return Int(int(i) + int(other.(Int)))
}

// ベンチマーク1: 個別関数
func BenchmarkIndividualFunc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = addInt(10, 20)
    }
}

// ベンチマーク2: ジェネリクス
func BenchmarkGenerics(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Add(10, 20)
    }
}

// ベンチマーク3: インターフェース
func BenchmarkInterface(b *testing.B) {
    x := Int(10)
    y := Int(20)
    for i := 0; i < b.N; i++ {
        _ = x.Add(y)
    }
}

/*
ベンチマーク結果の例:
BenchmarkIndividualFunc-8    1000000000    0.25 ns/op
BenchmarkGenerics-8          1000000000    0.25 ns/op
BenchmarkInterface-8         500000000     3.50 ns/op

結論:
- 個別関数とジェネリクスはほぼ同じパフォーマンス(インライン化される)
- インターフェースは仮想関数呼び出しのオーバーヘッドがある
- パフォーマンスが重要な場合は個別関数かジェネリクスを選ぶ
*/

トレードオフ分析

  • 個別関数: 最も高速だが、型ごとにコードが増える。小規模なプロジェクトや特定の型のみを扱う場合に最適
  • ジェネリクス: コンパイル時に型が解決されるため高速。コードの再利用性が高い。Go 1.18以降で推奨
  • インターフェース: 実行時の柔軟性が高いが、若干のオーバーヘッドあり。プラグインシステムなど動的な型が必要な場合に使用

---

チャレンジ2の解答: ポインタとメモリ効率

他言語との比較

# Python - すべてが参照渡し(オブジェクトへの参照)
class LargeStruct:
    def __init__(self):
        self.data = [i for i in range(1000)]

def process(s):
    return sum(s.data)

# 常に参照が渡される(コピーは発生しない)
large = LargeStruct()
result = process(large)

// JavaScript - オブジェクトは参照渡し
class LargeStruct {
    constructor() {
        this.data = Array.from({length: 1000}, (_, i) => i);
    }
}

function process(s) {
    return s.data.reduce((a, b) => a + b, 0);
}

// 常に参照が渡される
const large = new LargeStruct();
const result = process(large);

Go - 明示的な選択

package main

import (
    "fmt"
    "runtime"
    "time"
    "unsafe"
)

// 大きな構造体(8KB)
type LargeStruct struct {
    Data [1000]int  // 1000 * 8バイト = 8000バイト
}

// 中程度の構造体(40バイト)
type MediumStruct struct {
    A, B, C, D, E int  // 5 * 8バイト = 40バイト
}

// 小さな構造体(16バイト)
type SmallStruct struct {
    X, Y int  // 2 * 8バイト = 16バイト
}

// アプローチ1: 値渡し(コピーが発生)
func processByValue(s LargeStruct) int {
    // s はコピー。元の構造体とは別のメモリ領域
    // この関数内での変更は呼び出し元に影響しない
    sum := 0
    for _, v := range s.Data {
        sum += v
    }
    return sum
}

// アプローチ2: ポインタ渡し(参照のみコピー)
func processByPointer(s *LargeStruct) int {
    // s はポインタ(8バイト)。元の構造体を参照
    // メモリコピーが発生しない
    sum := 0
    for _, v := range s.Data {
        sum += v
    }
    return sum
}

// アプローチ3: 構造体の一部を変更する場合(値渡し)
func modifyByValue(s LargeStruct) LargeStruct {
    // 変更したコピーを返す必要がある
    s.Data[0] = 999
    return s  // さらにコピーが発生
}

// アプローチ4: 構造体の一部を変更する場合(ポインタ渡し)
func modifyByPointer(s *LargeStruct) {
    // ポインタ経由で直接変更
    // 返り値不要
    s.Data[0] = 999
}

func main() {
    // 構造体のサイズを確認
    fmt.Printf("LargeStruct size: %d bytes\n", unsafe.Sizeof(LargeStruct{}))
    fmt.Printf("Pointer size: %d bytes\n", unsafe.Sizeof(&LargeStruct{}))

    large := LargeStruct{}
    for i := range large.Data {
        large.Data[i] = i
    }

    // ベンチマーク1: 値渡し
    var m1 runtime.MemStats
    runtime.ReadMemStats(&m1)
    start := time.Now()
    for i := 0; i < 100000; i++ {
        processByValue(large)
    }
    elapsed1 := time.Since(start)
    var m2 runtime.MemStats
    runtime.ReadMemStats(&m2)
    fmt.Printf("値渡し: %v, メモリ割り当て: %d MB\n",
        elapsed1, (m2.TotalAlloc-m1.TotalAlloc)/1024/1024)

    // ベンチマーク2: ポインタ渡し
    var m3 runtime.MemStats
    runtime.ReadMemStats(&m3)
    start = time.Now()
    for i := 0; i < 100000; i++ {
        processByPointer(&large)
    }
    elapsed2 := time.Since(start)
    var m4 runtime.MemStats
    runtime.ReadMemStats(&m4)
    fmt.Printf("ポインタ渡し: %v, メモリ割り当て: %d MB\n",
        elapsed2, (m4.TotalAlloc-m3.TotalAlloc)/1024/1024)

    // パフォーマンス比較
    ratio := float64(elapsed1) / float64(elapsed2)
    fmt.Printf("\nポインタ渡しは値渡しの %.2fx 高速\n", ratio)

    // 変更のテスト
    fmt.Println("\n--- 値渡しでの変更 ---")
    before := large.Data[0]
    modified := modifyByValue(large)
    fmt.Printf("元の値: %d, 変更後の元の値: %d, 返された値: %d\n",
        before, large.Data[0], modified.Data[0])

    fmt.Println("\n--- ポインタ渡しでの変更 ---")
    before = large.Data[0]
    modifyByPointer(&large)
    fmt.Printf("元の値: %d, 変更後の値: %d\n", before, large.Data[0])
}

小さな構造体の場合

package main

import (
    "fmt"
    "time"
)

type Point struct {
    X, Y int
}

// 小さな構造体は値渡しの方が効率的な場合もある
func distanceByValue(p1, p2 Point) float64 {
    dx := float64(p1.X - p2.X)
    dy := float64(p1.Y - p2.Y)
    return dx*dx + dy*dy
}

func distanceByPointer(p1, p2 *Point) float64 {
    dx := float64(p1.X - p2.X)
    dy := float64(p1.Y - p2.Y)
    return dx*dx + dy*dy
}

func main() {
    p1 := Point{X: 10, Y: 20}
    p2 := Point{X: 30, Y: 40}

    // 小さな構造体(16バイト)のベンチマーク
    start := time.Now()
    for i := 0; i < 10000000; i++ {
        distanceByValue(p1, p2)
    }
    fmt.Println("値渡し:", time.Since(start))

    start = time.Now()
    for i := 0; i < 10000000; i++ {
        distanceByPointer(&p1, &p2)
    }
    fmt.Println("ポインタ渡し:", time.Since(start))
}

/*
小さな構造体の場合:
値渡し: 8ms
ポインタ渡し: 12ms

理由:
- 小さな構造体(16バイト以下)はCPUレジスタに収まる
- ポインタの間接参照のコストの方が高い
- スタック割り当てはヒープより高速
*/

メモリレイアウトの可視化

package main

import (
    "fmt"
    "unsafe"
)

type Example struct {
    A int32   // 4バイト
    B int64   // 8バイト
    C int32   // 4バイト
}

type OptimizedExample struct {
    B int64   // 8バイト
    A int32   // 4バイト
    C int32   // 4バイト
}

func main() {
    fmt.Println("=== メモリアライメント ===")

    // パディングを含むサイズ
    e := Example{}
    fmt.Printf("Example size: %d bytes\n", unsafe.Sizeof(e))
    fmt.Printf("  A offset: %d\n", unsafe.Offsetof(e.A))
    fmt.Printf("  B offset: %d\n", unsafe.Offsetof(e.B))
    fmt.Printf("  C offset: %d\n", unsafe.Offsetof(e.C))

    // 最適化されたレイアウト
    oe := OptimizedExample{}
    fmt.Printf("\nOptimizedExample size: %d bytes\n", unsafe.Sizeof(oe))
    fmt.Printf("  B offset: %d\n", unsafe.Offsetof(oe.B))
    fmt.Printf("  A offset: %d\n", unsafe.Offsetof(oe.A))
    fmt.Printf("  C offset: %d\n", unsafe.Offsetof(oe.C))

    /*
    出力例:
    Example size: 24 bytes
      A offset: 0
      B offset: 8
      C offset: 16

    OptimizedExample size: 16 bytes
      B offset: 0
      A offset: 8
      C offset: 12

    フィールドの順序を変えるだけで24バイト→16バイトに削減!
    */
}

よくある間違い

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

// 間違い1: ポインタレシーバーを使うべき場所で値レシーバー
func (u User) IncrementAgeBad() {
    u.Age++  // コピーに対する操作。元の値は変わらない
}

// 正しい方法: ポインタレシーバー
func (u *User) IncrementAge() {
    u.Age++  // ポインタ経由で元の値を変更
}

// 間違い2: nilポインタのチェック忘れ
func printUserBad(u *User) {
    fmt.Println(u.Name)  // u が nil ならパニック!
}

// 正しい方法: nilチェック
func printUser(u *User) {
    if u == nil {
        fmt.Println("User is nil")
        return
    }
    fmt.Println(u.Name)
}

// 間違い3: ループ内でのポインタの誤用
func collectUsersBad(names []string) []*User {
    users := make([]*User, 0, len(names))
    for _, name := range names {
        u := User{Name: name}
        users = append(users, &u)  // 危険!同じアドレスを追加
    }
    return users
}

// 正しい方法
func collectUsers(names []string) []*User {
    users := make([]*User, 0, len(names))
    for _, name := range names {
        u := User{Name: name}
        users = append(users, &u)  // 各イテレーションで新しいアドレス
    }
    return users
}

func main() {
    // テスト1
    user := User{Name: "Alice", Age: 25}
    user.IncrementAgeBad()
    fmt.Printf("Bad: %d\n", user.Age)  // 25(変わらない)
    user.IncrementAge()
    fmt.Printf("Good: %d\n", user.Age)  // 26

    // テスト2
    printUser(nil)  // 安全
    // printUserBad(nil)  // パニック

    // テスト3
    names := []string{"Alice", "Bob", "Charlie"}
    users := collectUsers(names)
    for _, u := range users {
        fmt.Println(u.Name)
    }
}

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

package main

import (
    "testing"
)

type Data struct {
    Values [1000]int
}

// ベンチマーク: 大きな構造体の値渡し
func BenchmarkLargeStructByValue(b *testing.B) {
    data := Data{}
    for i := range data.Values {
        data.Values[i] = i
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        processLargeByValue(data)
    }
}

// ベンチマーク: 大きな構造体のポインタ渡し
func BenchmarkLargeStructByPointer(b *testing.B) {
    data := Data{}
    for i := range data.Values {
        data.Values[i] = i
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        processLargeByPointer(&data)
    }
}

func processLargeByValue(d Data) int {
    sum := 0
    for _, v := range d.Values {
        sum += v
    }
    return sum
}

func processLargeByPointer(d *Data) int {
    sum := 0
    for _, v := range d.Values {
        sum += v
    }
    return sum
}

/*
実行コマンド:
go test -bench=. -benchmem

結果の例:
BenchmarkLargeStructByValue-8       10000    150000 ns/op    8192 B/op    1 allocs/op
BenchmarkLargeStructByPointer-8    100000     15000 ns/op       0 B/op    0 allocs/op

分析:
- ポインタ渡しは10倍高速
- 値渡しは毎回8KBの割り当てが発生
- ポインタ渡しはメモリ割り当てゼロ
*/

ルール・オブ・サム:

  • 構造体が小さい(< 数十バイト)→ 値渡し
  • 構造体を変更する必要がある → ポインタレシーバー
  • 構造体が大きい(> 数百バイト)→ ポインタ渡し
  • 一貫性のため、メソッドはすべてポインタレシーバーかすべて値レシーバーに統一
  • 疑問があれば、ポインタを使う(後で最適化可能)

---

チャレンジ3の解答: スライスの内部理解

他言語との比較

# Python - リストは動的配列
lst = []
for i in range(20):
    lst.append(i)
# Pythonは内部で自動的にメモリを拡張(実装はC言語)
# ユーザーは容量を意識する必要がない

// JavaScript - 配列は動的
const arr = [];
for (let i = 0; i < 20; i++) {
    arr.push(i);
}
// JavaScriptエンジンが自動で拡張

Goのスライス - 明示的なメモリ管理

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

// スライスの内部構造を可視化
func printSliceHeader(name string, s []int) {
    sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
    fmt.Printf("%s:\n", name)
    fmt.Printf("  Data: %p (配列の先頭アドレス)\n", unsafe.Pointer(sh.Data))
    fmt.Printf("  Len:  %d (現在の要素数)\n", sh.Len)
    fmt.Printf("  Cap:  %d (容量)\n", sh.Cap)
    fmt.Printf("  実際のサイズ: %d bytes\n\n", sh.Cap*int(unsafe.Sizeof(int(0))))
}

func main() {
    fmt.Println("=== スライスの成長パターン ===\n")

    // 容量1から開始
    s := make([]int, 0, 1)
    printSliceHeader("初期状態", s)

    // 要素を追加しながら容量の変化を観察
    for i := 0; i < 20; i++ {
        oldCap := cap(s)
        oldData := (*reflect.SliceHeader)(unsafe.Pointer(&s)).Data

        s = append(s, i)

        newCap := cap(s)
        newData := (*reflect.SliceHeader)(unsafe.Pointer(&s)).Data

        if oldCap != newCap {
            fmt.Printf("再割り当て発生!\n")
            fmt.Printf("  長さ: %d, 旧容量: %d → 新容量: %d\n",
                len(s), oldCap, newCap)
            fmt.Printf("  成長率: %.2fx\n", float64(newCap)/float64(oldCap))
            fmt.Printf("  配列アドレス変更: %p → %p\n\n",
                unsafe.Pointer(oldData), unsafe.Pointer(newData))
        }
    }

    printSliceHeader("最終状態", s)

    fmt.Println("=== Go 1.18以降の成長戦略 ===")
    fmt.Println("- 容量 < 256: 2倍に成長")
    fmt.Println("- 容量 >= 256: 約1.25倍に成長(より緩やか)")
    fmt.Println("- 理由: 大きなスライスでのメモリ浪費を防ぐ")
}

スライスとメモリリーク

package main

import (
    "fmt"
    "runtime"
)

// 問題: 大きなスライスの一部だけを返す
func getFirstTenBad(data []byte) []byte {
    // data全体の配列を参照し続ける
    // 元の配列がGCされない
    return data[:10]
}

// 解決策: 新しいスライスにコピー
func getFirstTenGood(data []byte) []byte {
    // 新しい配列を作成し、必要な部分だけコピー
    result := make([]byte, 10)
    copy(result, data[:10])
    // 元のdataは参照されず、GC可能
    return result
}

func demonstrateMemoryLeak() {
    // 100MBのデータ
    largeData := make([]byte, 100*1024*1024)
    for i := range largeData {
        largeData[i] = byte(i % 256)
    }

    // 悪い例: 100MBのメモリを保持し続ける
    _ = getFirstTenBad(largeData)

    // 良い例: 10バイトだけ保持
    _ = getFirstTenGood(largeData)
}

func main() {
    var m runtime.MemStats

    runtime.GC()
    runtime.ReadMemStats(&m)
    fmt.Printf("開始時: %d MB\n", m.Alloc/1024/1024)

    demonstrateMemoryLeak()

    runtime.GC()
    runtime.ReadMemStats(&m)
    fmt.Printf("処理後: %d MB\n", m.Alloc/1024/1024)
}

スライスの部分共有と意図しない変更

package main

import "fmt"

func demonstrateSharing() {
    // 元のスライス
    original := []int{1, 2, 3, 4, 5}
    fmt.Println("元のスライス:", original)

    // 部分スライス(同じ配列を共有)
    sub := original[1:4]
    fmt.Println("部分スライス:", sub)

    // 部分スライスを変更
    sub[0] = 999

    fmt.Println("\n部分スライスを変更後:")
    fmt.Println("元のスライス:", original)  // [1 999 3 4 5] - 変わった!
    fmt.Println("部分スライス:", sub)       // [999 3 4]

    // appendの動作
    fmt.Println("\n=== appendの動作 ===")
    a := make([]int, 3, 5)
    a[0], a[1], a[2] = 1, 2, 3
    fmt.Printf("a: %v (len=%d, cap=%d)\n", a, len(a), cap(a))

    b := a[:]  // 同じ配列を共有
    fmt.Printf("b: %v (len=%d, cap=%d)\n", b, len(b), cap(b))

    // 容量内でappend(配列を共有したまま)
    b = append(b, 4)
    fmt.Printf("\nappend後:")
    fmt.Printf("a: %v (len=%d, cap=%d)\n", a, len(a), cap(a))
    fmt.Printf("b: %v (len=%d, cap=%d)\n", b, len(b), cap(b))

    // もう一度変更
    b[0] = 999
    fmt.Printf("\nb[0]を変更後:")
    fmt.Printf("a: %v\n", a)  // aも変わる(同じ配列)
    fmt.Printf("b: %v\n", b)

    // 容量を超えるappend(新しい配列が割り当てられる)
    b = append(b, 5, 6)
    fmt.Printf("\n容量超過のappend後:")
    fmt.Printf("a: %v (len=%d, cap=%d)\n", a, len(a), cap(a))
    fmt.Printf("b: %v (len=%d, cap=%d)\n", b, len(b), cap(b))

    // もう一度変更
    b[0] = 111
    fmt.Printf("\nb[0]を変更後:")
    fmt.Printf("a: %v\n", a)  // aは変わらない(別の配列)
    fmt.Printf("b: %v\n", b)
}

func main() {
    demonstrateSharing()
}

事前割り当てによるパフォーマンス最適化

package main

import (
    "fmt"
    "testing"
)

// 悪い例: 容量を指定しない
func buildSliceBad() []int {
    var s []int  // nil slice, cap=0
    for i := 0; i < 10000; i++ {
        s = append(s, i)  // 何度も再割り当て
    }
    return s
}

// 良い例: 事前に容量を確保
func buildSliceGood() []int {
    s := make([]int, 0, 10000)  // 事前に容量確保
    for i := 0; i < 10000; i++ {
        s = append(s, i)  // 再割り当てなし
    }
    return s
}

// さらに良い例: 長さも指定してインデックスアクセス
func buildSliceBest() []int {
    s := make([]int, 10000)  // 長さと容量を確保
    for i := 0; i < 10000; i++ {
        s[i] = i  // appendより高速
    }
    return s
}

func BenchmarkBuildSliceBad(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buildSliceBad()
    }
}

func BenchmarkBuildSliceGood(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buildSliceGood()
    }
}

func BenchmarkBuildSliceBest(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buildSliceBest()
    }
}

/*
ベンチマーク結果:
BenchmarkBuildSliceBad-8     5000    250000 ns/op   386000 B/op   18 allocs/op
BenchmarkBuildSliceGood-8   10000    100000 ns/op    81920 B/op    1 allocs/op
BenchmarkBuildSliceBest-8   15000     80000 ns/op    81920 B/op    1 allocs/op

分析:
- Bad: 18回も再割り当てが発生、メモリ使用量も多い
- Good: 1回の割り当てで済む、2.5倍高速
- Best: インデックスアクセスでappendのオーバーヘッドを回避、さらに高速
*/

よくある間違い

package main

import "fmt"

func main() {
    fmt.Println("=== 間違い1: appendの結果を捨てる ===")
    s1 := []int{1, 2, 3}
    append(s1, 4)  // 結果を代入していない!
    fmt.Println(s1)  // [1 2 3] - 変わっていない

    s1 = append(s1, 4)  // 正しい
    fmt.Println(s1)  // [1 2 3 4]

    fmt.Println("\n=== 間違い2: スライスのコピーを忘れる ===")
    s2 := []int{1, 2, 3}
    s3 := s2  // 同じ配列を共有(コピーではない)
    s3[0] = 999
    fmt.Println("s2:", s2)  // [999 2 3] - s2も変わる
    fmt.Println("s3:", s3)  // [999 2 3]

    // 正しいコピー方法
    s4 := make([]int, len(s2))
    copy(s4, s2)
    s4[0] = 111
    fmt.Println("s2:", s2)  // [999 2 3] - s2は変わらない
    fmt.Println("s4:", s4)  // [111 2 3]

    fmt.Println("\n=== 間違い3: nilスライスとemptyスライスの混同 ===")
    var nilSlice []int
    emptySlice := []int{}
    madeSlice := make([]int, 0)

    fmt.Printf("nil slice: %v, len=%d, cap=%d, isNil=%v\n",
        nilSlice, len(nilSlice), cap(nilSlice), nilSlice == nil)
    fmt.Printf("empty slice: %v, len=%d, cap=%d, isNil=%v\n",
        emptySlice, len(emptySlice), cap(emptySlice), emptySlice == nil)
    fmt.Printf("made slice: %v, len=%d, cap=%d, isNil=%v\n",
        madeSlice, len(madeSlice), cap(madeSlice), madeSlice == nil)

    // すべて安全に使える
    for _, s := range [][]int{nilSlice, emptySlice, madeSlice} {
        s = append(s, 1)
        fmt.Println(s)
    }
}

スライスの内部構造の可視化

スライスの構造:

type slice struct {
    ptr *[cap]T  // 配列へのポインタ
    len int      // 現在の長さ
    cap int      // 容量
}

メモリレイアウト:

s := []int{1, 2, 3, 4, 5}

スライスヘッダ(24バイト on 64-bit):
┌─────────┬─────┬─────┐
│   ptr   │ len │ cap │
│  8 bytes│8bytes│8bytes│
└────┬────┴─────┴─────┘
     │
     └──> 実際の配列:
          ┌───┬───┬───┬───┬───┐
          │ 1 │ 2 │ 3 │ 4 │ 5 │
          └───┴───┴───┴───┴───┘

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

スライスヘッダ:
┌─────────┬─────┬─────┐
│   ptr   │ len │ cap │
│  (s+8)  │  3  │  4  │
└────┬────┴─────┴─────┘
     │
     └──> 同じ配列の別の位置:
          ┌───┬───┬───┬───┬───┐
          │ 1 │ 2 │ 3 │ 4 │ 5 │
          └───┴─┬─┴───┴───┴───┘
                └─ sub が指す位置

---

追加チャレンジ: ゼロ値を活用したコンフィグパターン

パターン1: デフォルト値の適用

package main

import "fmt"

type ServerConfig struct {
    Host    string
    Port    int
    Timeout int
    MaxConn int
    Debug   bool
}

// メソッドでデフォルト値を適用
func (c *ServerConfig) ApplyDefaults() {
    if c.Host == "" {
        c.Host = "localhost"
    }
    if c.Port == 0 {
        c.Port = 8080
    }
    if c.Timeout == 0 {
        c.Timeout = 30
    }
    if c.MaxConn == 0 {
        c.MaxConn = 100
    }
    // Debug のデフォルトは false なので、そのまま
}

func main() {
    // 一部だけ指定
    config := ServerConfig{
        Port:  3000,
        Debug: true,
    }

    config.ApplyDefaults()

    fmt.Printf("%+v\n", config)
    // {Host:localhost Port:3000 Timeout:30 MaxConn:100 Debug:true}
}

パターン2: Functional Options Pattern(実践的)

package main

import (
    "fmt"
    "time"
)

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
    debug   bool
}

// Option関数型
type Option func(*Server)

// オプション設定関数
func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConn(maxConn int) Option {
    return func(s *Server) {
        s.maxConn = maxConn
    }
}

func WithDebug(debug bool) Option {
    return func(s *Server) {
        s.debug = debug
    }
}

// コンストラクタ
func NewServer(opts ...Option) *Server {
    // デフォルト値
    s := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
        maxConn: 100,
        debug:   false,
    }

    // オプションを適用
    for _, opt := range opts {
        opt(s)
    }

    return s
}

func main() {
    // デフォルト値で作成
    s1 := NewServer()
    fmt.Printf("s1: %+v\n", s1)

    // 一部だけカスタマイズ
    s2 := NewServer(
        WithPort(3000),
        WithDebug(true),
    )
    fmt.Printf("s2: %+v\n", s2)

    // 複数のオプション
    s3 := NewServer(
        WithHost("0.0.0.0"),
        WithPort(9000),
        WithTimeout(60*time.Second),
        WithMaxConn(500),
    )
    fmt.Printf("s3: %+v\n", s3)
}

パターン3: ゼロ値で有効な型の設計

package main

import (
    "fmt"
    "sync"
)

// ゼロ値で使える型の良い例
type Counter struct {
    mu    sync.Mutex  // ゼロ値で使える
    count int         // ゼロ値は0
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// ゼロ値で使える設計の別の例
type Buffer struct {
    data []byte  // nil でも append は動作する
}

func (b *Buffer) Write(p []byte) {
    // b.data が nil でも動作する
    b.data = append(b.data, p...)
}

func (b *Buffer) Bytes() []byte {
    return b.data
}

func main() {
    // 初期化不要で使える
    var c Counter
    c.Increment()
    c.Increment()
    fmt.Println("Counter:", c.Value())

    var buf Buffer
    buf.Write([]byte("Hello"))
    buf.Write([]byte(" World"))
    fmt.Println("Buffer:", string(buf.Bytes()))
}

このパターンは標準ライブラリでも多用されています(bytes.Buffer, strings.Builder, sync.Mutexなど)。