0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-12

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

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

基本概念

配列 (Array)

  • 固定長のデータ構造
  • サイズはコンパイル時に決定
  • 値型(関数に渡すとコピーが作成される)
arr := [3]int{1, 2, 3}  // 要素数3の配列

スライス (Slice)

  • 可変長のデータ構造
  • 配列への「ビュー」として機能
  • 参照型(関数に渡すと元データを参照)
slice := []int{1, 2, 3}  // スライス

スライスの重要な特性

1. 可変長引数としての使用

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

正しい使用例:

func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

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

不正な例(コンパイルエラー):

func invalid(a ...int, b bool) {}  // 可変長が最後でない
func invalid(a ...int, b ...int) {} // 可変長引数が複数

2. 参照型としての振る舞い

  • スライスを関数に渡すと、元のデータを変更可能
func modify(s []int) {
    s[0] = 100  // 元のスライスを変更
}

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

3. 配列との比較

  • 配列は値型(関数に渡すとコピーが作成される)
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. スライスの初期化

// 長さ0、容量10のスライス
s := make([]int, 0, 10)

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

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

2. スライスのコピー

  • 単純な代入は参照をコピーするだけ
  • 完全なコピーにはcopy関数を使用
// 参照のコピー(危険)
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使用時には新しいスライスが返される可能性
s := make([]int, 2, 3)  // len=2, cap=3
s = append(s, 1)       // 容量内
s = append(s, 2)       // 容量超過→新しい配列が割り当て

ベストプラクティス

  1. 関数の引数:

    • 元データを変更したくない場合 → コピーを作成
    • 大きなデータを扱う場合 → スライス(コピーコスト回避)
  2. スライスの容量:

    • 要素数が事前にわかっている場合 → makeで初期容量を指定
  3. nilチェック:

    • スライスがnilかどうか確認する必要がある場合
    if slice == nil {
        // nilスライスの処理
    }
    
  4. 空スライス表現:

    • 明確に空を表現したい場合 → []int{}を使用

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

1. メモリリークの危険性

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

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

対策:必要な部分だけコピーして返す

func getSafeSlice() []byte {
    bigData := make([]byte, 1000000)
    smallData := make([]byte, 10)
    copy(smallData, bigData[:10])
    return smallData
}

2. 容量拡張の挙動

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

append時の容量拡張アルゴリズム:

  • 1024要素までは2倍ずつ拡張
  • それ以降は約1.25倍ずつ拡張
var s []int
for i := 0; i < 10000; i++ {
    s = append(s, i)  // 複数回のメモリ再割り当てが発生
}

最適化

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

スライス操作の落とし穴

1. 部分スライスの共有問題

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] 元データも変更される

安全な部分スライス取得

// 方法1: 完全なコピーを作成
safeSub := make([]int, 2)
copy(safeSub, original[1:3])

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

2. スライスの比較

スライスは直接比較できない:

a := []int{1, 2, 3}
b := []int{1, 2, 3}
// if a == b { ... }  // コンパイルエラー

比較方法

// 方法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から同時にアクセスするとデータ競合が発生:

var s []int

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

安全な使用方法

// 方法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からの読み取りは安全:

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. スライスの事前割り当て

// 非効率的
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. スライスの再利用

// バッファプールの使用例
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. 大きなスライスのメモリ解放

bigSlice := make([]int, 1e6)
// 使用後...
bigSlice = nil  // GCがメモリを解放できるように

特殊なスライス操作

1. スライスの削除

// インデックス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. スライスのフィルタリング

// 条件に合わない要素を削除
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. スライスの反転

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. スライスのスタック/キュー操作

// スタックとして使用(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. スライスのバッチ処理

// 大きなスライスをバッチ処理
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. メモリ割り当てのベンチマーク

// 悪い例(都度拡張)
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. スライスのメモリフットプリント削減

// 不要なメモリ保持を解放
func processLargeData() {
    largeData := loadHugeData() // 大きなデータ読み込み
    
    // 処理後に不要なメモリを解放
    result := process(largeData)
    largeData = nil  // GCが回収できるように
    
    return result
}

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

1. スライス範囲外アクセス防止

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スライスと空スライスの適切な扱い

func processSlice(s []int) {
    if s == nil {
        // nilスライス特有の処理
        return
    }
    if len(s) == 0 {
        // 空スライス処理
        return
    }
    // 通常処理
}

標準ライブラリの活用

1. sortパッケージの利用

// スライスのソート
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の効率的な使用

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

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

1. 設定値のマージ

// デフォルト設定とユーザー設定をマージ
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. バイナリデータのチャンク処理

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. スライスの内部構造調査

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. メモリリーク検出

```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パッケージ

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. ゼロアロケーションスライス

// 空スライスの最適化表現
var zeroAlloc []int  // nilスライス
empty := []int{}     // 空スライス(Go 1.21以降で最適化)

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

実践的なアドバイス

  1. API設計のポイント:

    • 関数がスライスを変更するかどうかをドキュメントに明記
    • 可能な場合、スライスではなくインターフェースを返すことを検討
  2. テストの重要性:

    • nilスライス、空スライス、容量不足の場合などエッジケースをテスト
    • ベンチマークテストでパフォーマンスを計測
  3. プロファイリング:

    • pprofを使用してスライス操作のメモリ使用量を分析
    • 不必要な割り当てがないか定期的にチェック
  4. コードレビュー時のチェックポイント:

    • スライスの容量が適切か
    • 並行処理での安全対策があるか
    • 大きなデータのコピーが発生していないか
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?