Go言語のスライスと配列：重要な注意点


備忘録として、個人用に残してます。

# Go言語のスライスと配列：重要な注意点

## 基本概念

### 配列 (Array)
- **固定長**のデータ構造
- サイズはコンパイル時に決定
- 値型（関数に渡すとコピーが作成される）

```go
arr := [3]int{1, 2, 3}  // 要素数3の配列
```

### スライス (Slice)
- **可変長**のデータ構造
- 配列への「ビュー」として機能
- 参照型（関数に渡すと元データを参照）

```go
slice := []int{1, 2, 3}  // スライス
```

---

## スライスの重要な特性

### 1. 可変長引数としての使用
- 関数の最後の引数のみ可変長(`...T`)に可能
- スライスを展開して渡すには`...`を使用

**正しい使用例:**
```go
func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

nums := []int{1, 2, 3}
sum(nums...)  // スライスを展開
```

**不正な例（コンパイルエラー）:**
```go
func invalid(a ...int, b bool) {}  // 可変長が最後でない
func invalid(a ...int, b ...int) {} // 可変長引数が複数
```

### 2. 参照型としての振る舞い
- スライスを関数に渡すと、元のデータを変更可能

```go
func modify(s []int) {
    s[0] = 100  // 元のスライスを変更
}

func main() {
    s := []int{1, 2, 3}
    modify(s)
    fmt.Println(s)  // [100 2 3]
}
```

### 3. 配列との比較
- 配列は値型（関数に渡すとコピーが作成される）

```go
func modifyArray(a [3]int) {
    a[0] = 100  // コピーを変更
}

func main() {
    a := [3]int{1, 2, 3}
    modifyArray(a)
    fmt.Println(a)  // [1 2 3]（変更されない）
}
```

---

## 注意すべき操作

### 1. スライスの初期化
```go
// 長さ0、容量10のスライス
s := make([]int, 0, 10)

// nilスライス
var nilSlice []int

// 空スライス
emptySlice := []int{}
```

### 2. スライスのコピー
- 単純な代入は参照をコピーするだけ
- 完全なコピーには`copy`関数を使用

```go
// 参照のコピー（危険）
s1 := []int{1, 2, 3}
s2 := s1
s2[0] = 100  // s1も変更される

// 安全なコピー
s3 := make([]int, len(s1))
copy(s3, s1)
s3[0] = 200  // s1は変更されない
```

### 3. スライスの拡張
- `append`使用時には新しいスライスが返される可能性
```go
s := make([]int, 2, 3)  // len=2, cap=3
s = append(s, 1)       // 容量内
s = append(s, 2)       // 容量超過→新しい配列が割り当て
```

---

## ベストプラクティス

1. **関数の引数**:
   - 元データを変更したくない場合 → コピーを作成
   - 大きなデータを扱う場合 → スライス（コピーコスト回避）

2. **スライスの容量**:
   - 要素数が事前にわかっている場合 → `make`で初期容量を指定

3. **nilチェック**:
   - スライスがnilかどうか確認する必要がある場合
   ```go
   if slice == nil {
       // nilスライスの処理
   }
   ```

4. **空スライス表現**:
   - 明確に空を表現したい場合 → `[]int{}`を使用

## メモリ管理に関する注意点

### 1. メモリリークの危険性
スライスが大きな配列の一部を参照し続けると、GCが働かずメモリリークの原因に：

```go
func getBigSlice() []byte {
    bigData := make([]byte, 1000000)
    return bigData[:10]  // 10要素だけ返すが、背後で全体を保持
}
```

**対策**：必要な部分だけコピーして返す
```go
func getSafeSlice() []byte {
    bigData := make([]byte, 1000000)
    smallData := make([]byte, 10)
    copy(smallData, bigData[:10])
    return smallData
}
```

### 2. 容量拡張の挙動

こちらは、変更の可能性ありなので
最新仕様を確認してください。

`append`時の容量拡張アルゴリズム：
- 1024要素までは2倍ずつ拡張
- それ以降は約1.25倍ずつ拡張

```go
var s []int
for i := 0; i < 10000; i++ {
    s = append(s, i)  // 複数回のメモリ再割り当てが発生
}
```

**最適化**：
```go
s := make([]int, 0, 10000)  // 事前に容量確保
for i := 0; i < 10000; i++ {
    s = append(s, i)
}
```

---

## スライス操作の落とし穴

### 1. 部分スライスの共有問題
```go
original := []int{1, 2, 3, 4, 5}
sub := original[1:3]  // [2, 3]

// subに変更を加えると...
sub[0] = 99
fmt.Println(original)  // [1, 99, 3, 4, 5] 元データも変更される
```

**安全な部分スライス取得**：
```go
// 方法1: 完全なコピーを作成
safeSub := make([]int, 2)
copy(safeSub, original[1:3])

// 方法2: appendを利用（Go 1.21以降）
safeSub := append([]int(nil), original[1:3]...)
```

### 2. スライスの比較
スライスは直接比較できない：
```go
a := []int{1, 2, 3}
b := []int{1, 2, 3}
// if a == b { ... }  // コンパイルエラー
```

**比較方法**：
```go
// 方法1: reflect.DeepEqual
if reflect.DeepEqual(a, b) { ... }

// 方法2: 手動で比較（パフォーマンス重視時）
func slicesEqual(a, b []int) bool {
    if len(a) != len(b) {
        return false
    }
    for i := range a {
        if a[i] != b[i] {
            return false
        }
    }
    return true
}
```

---

## 並行処理での注意

### 1. スライスはスレッドセーフではない
複数のgoroutineから同時にアクセスするとデータ競合が発生：

```go
var s []int

func appendData() {
    for i := 0; i < 1000; i++ {
        go func(n int) {
            s = append(s, n)  // 競合状態
        }(i)
    }
}
```

**安全な使用方法**：
```go
// 方法1: sync.Mutexを使用
var (
    s   []int
    mut sync.Mutex
)

func safeAppend(n int) {
    mut.Lock()
    defer mut.Unlock()
    s = append(s, n)
}

// 方法2: channelを使用
results := make(chan int, 1000)
go func() {
    for n := range results {
        s = append(s, n)
    }
}()
```

### 2. スライスの読み取り専用使用
複数goroutineからの読み取りは安全：
```go
func printSlice(s []int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println(s)  // 読み取りのみなら安全
}

func main() {
    s := []int{1, 2, 3}
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go printSlice(s, &wg)
    }
    wg.Wait()
}
```

---

## パフォーマンスチューニング

### 1. スライスの事前割り当て
```go
// 非効率的
var s []string
for _, item := range items {
    s = append(s, process(item))
}

// 効率的
s := make([]string, 0, len(items))
for _, item := range items {
    s = append(s, process(item))
}
```

### 2. スライスの再利用
```go
// バッファプールの使用例
var bufPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 0, 1024)
    },
}

func getBuffer() []byte {
    return bufPool.Get().([]byte)
}

func putBuffer(buf []byte) {
    buf = buf[:0]  // リセット
    bufPool.Put(buf)
}
```

### 3. 大きなスライスのメモリ解放
```go
bigSlice := make([]int, 1e6)
// 使用後...
bigSlice = nil  // GCがメモリを解放できるように
```

---

## 特殊なスライス操作

### 1. スライスの削除
```go
// インデックスiの要素を削除
func remove(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// 順序を保持しない高速削除
func fastRemove(s []int, i int) []int {
    s[i] = s[len(s)-1]
    return s[:len(s)-1]
}
```

### 2. スライスのフィルタリング
```go
// 条件に合わない要素を削除
s := []int{1, 2, 3, 4, 5}
n := 0
for _, x := range s {
    if x%2 == 1 {  // 奇数だけ残す
        s[n] = x
        n++
    }
}
s = s[:n]
```

### 3. スライスの反転
```go
func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}
```

---

## まとめチェックリスト

✅ スライスは参照型、配列は値型と理解している  
✅ 大きなデータを扱う時はメモリ使用に注意している  
✅ `append`の容量拡張挙動を理解している  
✅ 並行処理では適切な同期機構を使用している  
✅ スライスのコピーが必要な場面で`copy`を使っている  
✅ 部分スライスが元データを参照していることを意識している  
✅ スライスの事前割り当てでパフォーマンスを最適化している  
✅ 不要なメモリ保持を避けるため、大きなスライスは`nil`を代入している。

# Go言語のスライスと配列：実践的な応用テクニック

## 高度なスライス操作

### 1. スライスのスタック/キュー操作
```go
// スタックとして使用（LIFO）
stack := []int{1, 2, 3}
// Push
stack = append(stack, 4) 
// Pop
val := stack[len(stack)-1]
stack = stack[:len(stack)-1]

// キューとして使用（FIFO）
queue := []int{1, 2, 3}
// Enqueue
queue = append(queue, 4)
// Dequeue
val := queue[0]
queue = queue[1:]  // メモリリークに注意！
```

### 2. スライスのバッチ処理
```go
// 大きなスライスをバッチ処理
data := make([]int, 10000)
batchSize := 100

for i := 0; i < len(data); i += batchSize {
    end := i + batchSize
    if end > len(data) {
        end = len(data)
    }
    batch := data[i:end]
    processBatch(batch)
}
```

## パフォーマンス最適化の深堀り

### 1. メモリ割り当てのベンチマーク
```go
// 悪い例（都度拡張）
func BenchmarkAppend(b *testing.B) {
    var s []int
    for i := 0; i < b.N; i++ {
        s = append(s, i)
    }
}

// 良い例（事前割り当て）
func BenchmarkPreAllocate(b *testing.B) {
    s := make([]int, 0, b.N)
    for i := 0; i < b.N; i++ {
        s = append(s, i)
    }
}
```

### 2. スライスのメモリフットプリント削減
```go
// 不要なメモリ保持を解放
func processLargeData() {
    largeData := loadHugeData() // 大きなデータ読み込み
    
    // 処理後に不要なメモリを解放
    result := process(largeData)
    largeData = nil  // GCが回収できるように
    
    return result
}
```

## エラーハンドリングのベストプラクティス

### 1. スライス範囲外アクセス防止
```go
func safeAccess(s []int, index int) (int, error) {
    if index < 0 || index >= len(s) {
        return 0, fmt.Errorf("index out of range: %d", index)
    }
    return s[index], nil
}
```

### 2. nilスライスと空スライスの適切な扱い
```go
func processSlice(s []int) {
    if s == nil {
        // nilスライス特有の処理
        return
    }
    if len(s) == 0 {
        // 空スライス処理
        return
    }
    // 通常処理
}
```

## 標準ライブラリの活用

### 1. sortパッケージの利用
```go
// スライスのソート
nums := []int{3, 1, 4, 1, 5, 9}
sort.Ints(nums)

// カスタムソート
users := []struct{
    Name string
    Age  int
}{
    {"Alice", 25},
    {"Bob", 30},
}
sort.Slice(users, func(i, j int) bool {
    return users[i].Age < users[j].Age
})
```

### 2. bytes.Bufferの効率的な使用
```go
// 文字列結合の効率的な方法
var buf bytes.Buffer
for _, s := range []string{"a", "b", "c"} {
    buf.WriteString(s)
}
result := buf.String()
```

## リアルワールドユースケース

### 1. 設定値のマージ
```go
// デフォルト設定とユーザー設定をマージ
func mergeConfig(defaults, overrides []Config) []Config {
    merged := make([]Config, len(defaults))
    copy(merged, defaults)
    
    for _, override := range overrides {
        if override.ID < len(merged) {
            merged[override.ID] = override
        }
    }
    return merged
}
```

### 2. バイナリデータのチャンク処理
```go
func processInChunks(data []byte, chunkSize int) error {
    for offset := 0; offset < len(data); offset += chunkSize {
        end := offset + chunkSize
        if end > len(data) {
            end = len(data)
        }
        chunk := data[offset:end]
        if err := processChunk(chunk); err != nil {
            return err
        }
    }
    return nil
}
```

## デバッグテクニック

### 1. スライスの内部構造調査
```go
func inspectSlice(s []int) {
    fmt.Printf("Pointer: %p, Length: %d, Capacity: %d\n", 
        &s[0], len(s), cap(s))
}

// 使用例
s := make([]int, 3, 5)
inspectSlice(s) // 内部状態を出力
```

### 2. メモリリーク検出
```runtimeパッケージを使用してメモリ使用量を監視:
```go
func printMemUsage() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("Alloc = %v MiB", m.Alloc/1024/1024)
}
```

## 新しいのGoバージョンでの改善

### 1. Go 1.21の新しいslicesパッケージ
```go
import "slices"

// 新しい便利関数群
nums := []int{1, 2, 3}
if slices.Contains(nums, 2) {
    fmt.Println("Contains 2")
}

// スライスの比較
other := []int{1, 2, 3}
if slices.Equal(nums, other) {
    fmt.Println("Equal")
}
```

### 2. ゼロアロケーションスライス
```go
// 空スライスの最適化表現
var zeroAlloc []int  // nilスライス
empty := []int{}     // 空スライス（Go 1.21以降で最適化）

// パフォーマンス重視の場合はnilスライスを使用
```

---

## 実践的なアドバイス

1. **API設計のポイント**:
   - 関数がスライスを変更するかどうかをドキュメントに明記
   - 可能な場合、スライスではなくインターフェースを返すことを検討

2. **テストの重要性**:
   - nilスライス、空スライス、容量不足の場合などエッジケースをテスト
   - ベンチマークテストでパフォーマンスを計測

3. **プロファイリング**:
   - `pprof`を使用してスライス操作のメモリ使用量を分析
   - 不必要な割り当てがないか定期的にチェック

4. **コードレビュー時のチェックポイント**:
   - スライスの容量が適切か
   - 並行処理での安全対策があるか
   - 大きなデータのコピーが発生していないか