30
21

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

【Go】【チートシート】Slice Tricksを作ってみた

Last updated at Posted at 2023-07-12

はじめに

スライスマスターになりたいので、Slice Tricks(チートシート)を作ってみました!!!💪

元ネタはGo(Golang)公式WikiにあるGo SliceTricksです。

※以下では、Typeを( T )と略して書いています。
※自分の中で、よく使いそうなものに🏆をつけています。
※目次を見ながら、記事を見ていただけると見やすいかなと思います!!!🙆

1. スライスの定義 ・ 初期化

// スライスの定義: 1
var s []int
fmt.Println(s) // []

// スライスの定義: 2
s = []int{1, 2, 3}
fmt.Println(s) // [1 2 3]

// スライスの定義: 3
s = make([]int, 3)
fmt.Println(s) // [0 0 0]

// スライスの定義: 4
s = make([]int, 3, 5)
fmt.Println(s) // [0 0 0]

// スライスの定義: 5
s = []int{1: 5, 4: 10}
fmt.Println(s) // [0 5 0 0 10]

始めは、配列やマップと混同した💦
一応、配列とmapも書いておく

// 配列の定義: 1
var a [3]int
fmt.Println(a) // [0 0 0]

// 配列の定義: 2
a = [3]int{1, 2, 3}
fmt.Println(a) // [1 2 3]

// 配列の定義: 3
a = [...]int{1, 2, 3}
fmt.Println(a) // [1 2 3]

// mapの定義: 1
var m map[string]int
fmt.Println(m) // map[]

// mapの定義: 2
m = map[string]int{"x": 10, "y": 20}
fmt.Println(m) // map[x:10 y:20]

// mapの定義: 3
m = make(map[string]int)
fmt.Println(m) // map[]

2. スライスの要素の取得

s := []int{1, 2, 3}
fmt.Println(s[0]) // 1
fmt.Println(s[1]) // 2
fmt.Println(s[2]) // 3

3. 🏆 スライスのコピー

ここからがSlice Trickの本題!!!!

  1. b = make([]T, len(a))
    copy(b, a)

    • copy()関数を用いてaの全要素をbにコピー
  2. b = append([]T(nil), a...)

    • aの要素は新しいスライスにコピーされ、そのコピーされたスライスがbに代入される
  3. b = append(a[:0:0], a...)

    • a[:0:0]は、スライスaから新たなスライス( aと同じcap(容量)を持つがlen(長さ)が0の新たなスライス )を作成
    • そして、そのスライスにaの全要素を追加

00.53.49.png

s1 := []int{1, 2, 3, 4, 5}
b1 := make([]int, len(s1))
n := copy(b1, s1)

fmt.Println(n, b1) // 5 [1 2 3 4 5]

s2 := []int{1, 2, 3, 4, 5}
b2 := append([]int(nil), s2...)

fmt.Println(b2) // 出力: [1 2 3 4 5]

s3 := []int{1, 2, 3, 4, 5}
b3 := append(s3[:0:0], s3...)

fmt.Println(b3) // 出力: [1 2 3 4 5]

💡テクいポイント: appendcopyについて

この記事を書いて気づいたのですが、、、

cap(容量)が足りなくなると

appendは、新たなメモリ領域を確保し、元のスライスを確保したメモリ領域にコピーをします。

copyは、新たなメモリ領域を確保をせずに、元のスライスを新たなスライスにコピーします。

→つまり、appendでは、新たなメモリ領域を確保(メモリアロケーション)するので、パフォーマンスに影響がありそう💦
→できるだけ、copy関数を用いてスライスの操作を行う方がいいと感じます!

結論 : appendcopyは、パフォーマンス検証しながら、適切な方法を選択することが重要ですね🙆‍♂️

  • 大きなスライスを扱う時
    copy関数でスライスの操作を行う方がいいかも
    理由appendだと不必要なメモリの確保と解放が多発(ガベージコレクション(GC)が頻繁に発生)し、パフォーマンスが低下する。

  • 小さいスライスを扱う時
    append関数でスライスの操作を行う方がいいかも
    理由:コードの読みやすさやメンテナンス性が上がるかも(append関数のほうが他コードや記事でよく見る👀)

この先からは、appendcopyの振る舞いに注目して読み進めるといいかも🤔

4. 🏆 スライスの要素追加, 結合

  1. for _, i := range b { a = append(a, i) }

  2. a = append(a, b...)

00.54.04.png

s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
for _, i := range s2 {
    s1 = append(s1, i)
}

fmt.Println(s1) // [1 2 3 4 5 6]

s3 := []int{1, 2, 3}
s4 := []int{4, 5, 6}
s3 = append(s3, s4...)

fmt.Println(s3) // [1 2 3 4 5 6]

💡テクいポイント: cap(容量)を超えてしまったら?

スライス b の全要素をスライス a の末尾に追加するとき、aのcap(容量)を超えてしまったらどうなるか

00.56.54.png

→ 元のスライスの2倍のcap(容量)を持つ新しいスライスが作成され、そこにaのコピーと、末尾にbが追加される

00.56.10.png

このため、元のスライスの要素数が非常に大きい場合や、追加する要素数が多い場合は、必要となるメモリ量とデータのコピーにかかる時間が増加し、パフォーマンスに影響が出ます💦

対策 : 事前に必要なスライスのサイズが分かっている場合、または大量のデータを扱う場合は、make 関数を使って十分なcap(容量)を持つスライスを事前に確保しておくことが大切です!

💡テクいポイント: コピーするか直接読み出すか

大きいなスライスを扱う場合は、for i := range sliceを使う!

for i, v := range map[] or sliceを使った場合
i, vにはコピーの操作が走るため、大きなデータになるほど遅くなってしまう💦

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

// インデックス(i)とその値のコピー(v)を生成
for i, v := range s {
    fmt.Println(i) 
    fmt.Println(v)
}

// スライスから直接値を読み出す
for i := range s {
    fmt.Println(s[i]) 
}

大きいスライスを扱う場合は、for i := range sliceを使う!
iを使って、slice[i]で要素にアクセス(※mapも同じ)

5. スライスの要素先頭、末尾削除

  1. x, a = a[0], a[1:]

00.58.19.png

  1. x, a = a[len(a)-1], a[:len(a)-1]

00.58.03.png

// 最初の要素削除
s1 := []int{1, 2, 3, 4, 5}
_, s1 = s1[0], s1[1:]

fmt.Println(s1) // [2 3 4 5]

// 末尾の要素削除
s2 := []int{1, 2, 3, 4, 5}
_, s2 = s2[len(s2)-1], s2[:len(s2)-1]

fmt.Println(s2) // [1 2 3 4]

6. スライスの要素カット、削除

  1. a = append(a[:i], a[j:]...)
    • a[:i] は、 i 番目の要素の直前までのスライス
    • a[j:] は、 j 番目の要素から最後までのスライス
    • append を使って、2つのスライスを連結!!

00.58.47.png

  1. a = append(a[:i], a[i+1:]...)

  2. a = a[:i+copy(a[i:], a[i+1:])]

    • a[i:] は、 i 番目の要素から最後までのスライス
    • a[i+1:] は、i+1 番目の要素から最後までのスライス
    • copy 関数は、第2引数のスライスから第1引数のスライスに要素をコピー
    • a[:i+copy(a[i:], a[i+1:])] で、copy関数がコピーした要素数だけスライスし直し、末尾の余分な要素(i番目の要素のコピー)が削除される

00.59.05.png

// スライスの要素をカット
// cut
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

i := 3
j := 7
a = append(a[:i], a[j:]...)

fmt.Println(a) // [1 2 3 8 9 10]

// スライスの要素削除
// append関数を使った要素削除
s := []int{1, 2, 3, 4, 5}
i = 2
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [1 2 4 5]

// copy関数を使った要素削除
s = []int{1, 2, 3, 4, 5}

i = 2
s = s[:i+copy(s[i:], s[i+1:])]

fmt.Println(s) // [1 2 4 5]

7. スライスの順序を保持しない要素削除

  1. a[i] = a[len(a)-1]
    a = a[:len(a)-1]
    • i番目の要素 を スライスaの最後の要素と置き換え
    • スライスの最後の要素(元のi番目の要素)を削除

00.59.34.png

// 順序を保持しない要素削除
s := []int{1, 2, 3, 4, 5}
i := 2
s[i] = s[len(s)-1]
s = s[:len(s)-1]

fmt.Println(s) // [1 2 5 4]

💡テクいポイント

要素の削除を高速に行う必要がある場合に有効!
理由は、スライスの最後の要素を削除するとき中間の要素を削除するよりも高速!!!🚅

8. スライスの拡張

  1. a = append(a, make([]T, j)...)
    • make([]T, j) で、新しいスライスを作成
    • j 個のゼロ値で初期化するので、例えば int の場合は 0になる
      • ( string の場合は空文字 ""、boolean の場合は falseになるよー )
    • append(a, make([]T, j)...) は 作成した新しいスライスを、元のスライス a の末尾に追加

01.13.36.png

  1. a = append(a[:i], append(make([]T, j), a[i:]...)...)
    • make([]T, j) で、新しいスライスを作成
    • append(make([]T, j), a[i:]...) で、新しく作成したゼロ値のスライスと a の i 番目の要素から最後までのスライスを結合
    • append(a[:i], ...) は、a の i 番目の要素の直前までのスライスと結合したスライスを、再度結合します。(結合に、結合を重ねる。。。ややこし💦)
    • 結合を知らんという方は、4. スライスの要素追加, 結合へ!

01.00.19.png

// スライスの要素の拡張: 1
s := []int{1, 2, 3, 4, 5}
i := 5
s = append(s, make([]int, i)...)

fmt.Println(s) // [1 2 3 4 5 0 0 0 0 0]

// スライスの要素の拡張: 2
s = []int{1, 2, 3, 4, 5}
i = 5
j := 2
s = append(s[:j], append(make([]int, i), s[j:]...)...)

fmt.Println(s) // [1 2 0 0 0 0 0 3 4 5]

9. スライスを逆順にする

01.05.33.png

// スライスを逆順にする: 1
s1 := []int{1, 2, 3, 4, 5}
for i := len(s1)/2 - 1; i >= 0; i-- {
    opp := len(s1) - 1 - i
    s1[i], s1[opp] = s1[opp], s1[i]
}

fmt.Println(s1) // [5 4 3 2 1]

// スライスを逆順にする: 2
s2 := []int{1, 2, 3, 4, 5}
for left, right := 0, len(s2)-1; left < right; left, right = left+1, right-1 {
    s2[left], s2[right] = s2[right], s2[left]
}

fmt.Println(s2) // [5 4 3 2 1]

10. 🏆 スライスをソートする

  1. sort.Slice(a, func(i, j T) bool { return a[i] < a[j] })

    • 不安定ソート: 並び替えを行う際に、同じ値を持つ要素があっても、元の順序を気にせず相対的な順序になる
  2. sort.SliceStable(a, func(i, j T) bool { return a[i] < a[j] })

    • 安定ソート: 並び替えを行う際に、同じ値を持つ要素があったら、元の順序を保持する
  3. sort.Ints(a)

    • 不安定ソート: sort.IntSlice(a) を作成し、その上で sort.Sort を実行している。
  4. sort.Stable(sort.IntSlice(a))

    • 安定ソート: sort.IntSlice(a) を作成し、その上で sort.Stable を実行している。

01.16.32.png

// スライスを不安定ソート
s1 := []int{3, 4, 5, 1, 2}

sort.Slice(s1, func(i, j int) bool {
    return s1[i] < s1[j]
})

fmt.Println(s1) // [1 2 3 4 5]

// スライスを安定ソートする
s2 := []int{3, 4, 5, 1, 2}
sort.SliceStable(s2, func(i, j int) bool {
    return s2[i] < s2[j]
})

fmt.Println(s2) // [1 2 3 4 5]

// スライスを不安定ソート
s3 := []int{3, 4, 5, 1, 2}

sort.Ints(s3)

fmt.Println(s3) // [1 2 3 4 5]

// スライスを安定ソートする
s4 := []int{3, 4, 5, 1, 2}

sort.Stable(sort.IntSlice(s4))

fmt.Println(s4) // [1 2 3 4 5]

スライスを使って安定ソートと不安定ソートの違いを視覚的に理解するのは難しいので、本題とは外れますが、マップを使って説明します。。。🙏

// スライスを不安定ソート
p := []P{{"A", 20}, {"B", 20}, {"C", 25}, {"D", 20}}
sort.Slice(p, func(i, j int) bool { return p[i].a < p[j].a })
for _, v := range p {
    fmt.Printf("%s: %d\n", v.n, v.a)
}
// "A", "B", "D"(abcd順)の順序はソートごとに変わる可能性がある
// A: 20
// D: 20
// B: 20
// C: 25

// スライスを安定ソートする
p = []P{{"A", 20}, {"B", 20}, {"C", 25}, {"D", 20}}
sort.SliceStable(p, func(i, j int) bool { return p[i].a < p[j].a })
for _, v := range p {
    fmt.Printf("%s: %d\n", v.n, v.a)
}
// "A", "B", "D"(abcd順)の順序はソート前と同じ
// A: 20
// B: 20
// D: 20
// C: 25

11. スライスの要素をフィルタリング

01.17.22.png

// スライスの要素を偶数でフィルタリング(奇数のみ)
s1 := []int{1, 2, 3, 4, 5, 6}
for i := 0; i < len(s1); {
    if s1[i]%2 == 0 {
        s1 = append(s1[:i], s1[i+1:]...)
    } else {
        i++
    }
}

fmt.Println(s1) // [1 3 5]

// スライスの要素を奇数でフィルタリング(偶数のみ)
s2 := []int{1, 2, 3, 4, 5, 6}
for i := 0; i < len(s2); {
    if s2[i]%2 != 0 {
        s2 = append(s2[:i], s2[i+1:]...)
    } else {
        i++
    }
}

fmt.Println(s2) // [2 4 6]

12. スライスのシャッフル

01.06.20.png

// スライスをシャッフリングする
// 元のスライス s1 の要素を新しいスライス d1 にランダムな順序でコピー
s1 := []int{1, 2, 3, 4, 5}
d1 := make([]int, len(s1))
perm := rand.Perm(len(s1))
for i, v := range perm {
    d1[v] = s1[i]
}

fmt.Println(d1) // [3 5 1 4 2](ランダム)

// スライスをシャッフリングする: 2
// 追加のメモリを必要とせず、元のスライスを直接シャッフルするため、効率的かも
s2 := []int{1, 2, 3, 4, 5}
for i := len(s2) - 1; i > 0; i-- {
    j := rand.Intn(i + 1)
    s2[i], s2[j] = s2[j], s2[i]
}

fmt.Println(s2) // [5 2 1 3 4](ランダム)

13. 2次元スライスの定義・初期化

// 2次元スライスの定義:1
var s [][]int
fmt.Println(s) // []

// 2次元スライスの定義:2
s = [][]int{{1, 2}, {3, 4}, {5, 6}}
fmt.Println(s) // [[1 2] [3 4] [5 6]]

// 2次元スライスの定義:3
s1 := make([][]int, 5)
fmt.Println(s1) // [[] [] [] [] []]

// 2次元スライスの定義:4
s2 := make([][]int, 5, 10)
fmt.Println(s2) // [[] [] [] [] []]

// 2次元スライスの定義:5
s3 := make([][]int, 5)
for i := range s3 {
    s3[i] = make([]int, 3)
}
fmt.Println(s3) // [[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]]

s := [][]int{{1, 2}, {3, 4}, {5, 6}}
fmt.Println(s[1][1]) // 4

14. 2次元スライスの要素追加・結合

// 2次元スライスの要素追加
s := [][]int{{1, 2}, {3, 4}, {5, 6}}
s = append(s, []int{7, 8})

fmt.Println(s) // [[1 2] [3 4] [5 6] [7 8]]

// 2次元スライスの要素結合
s1 := [][]int{{1, 2}, {3, 4}, {5, 6}}
s2 := [][]int{{1, 2}, {3, 4}, {5, 6}}
s1 = append(s1, s2...)

fmt.Println(s1) // [[1 2] [3 4] [5 6] [1 2] [3 4] [5 6]]

15. 2次元スライスの要素削除

// 2次元スライスの要素削除
s1 := [][]int{{1, 2}, {3, 4}, {5, 6}}
i := 1
s1 = append(s1[:i], s1[i+1:]...)
fmt.Println(s1) // [[1 2] [5 6]]

// 2次元スライスの要素削除: 2
s2 := [][]int{{1, 2}, {3, 4}, {5, 6}}
i = 1
s2 = s2[:i+copy(s2[i:], s2[i+1:])]
fmt.Println(s2) // [[1 2] [5 6]]

16. スライスを任意の要素数に分割(バッチ処理する)

01.19.52.png

s := []int{1, 2, 3, 4, 5}
i := 2
d := make([][]int, 0, (len(s)+i-1)/i)
for i < len(s) {
    s, d = s[i:], append(d, s[0:i:i])
}
d = append(d, s)

fmt.Println(d) // [[1 2] [3 4] [5]]

// バッチ処理  一定量のデータを集め、一括処理するための処理方法
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// バッチサイズ  一度に処理するデータの数を指します。
batchSize := 3
batches := make([][]int, 0, (len(s)+batchSize-1)/batchSize)

for batchSize < len(s) {
    s, batches = s[batchSize:], append(batches, s[0:batchSize:batchSize])
}
batches = append(batches, s)
fmt.Println(batches) // [[0 1 2] [3 4 5] [6 7 8] [9]]

💡テクいポイント: aをbで割った時の商を"切り上げ"

aをbで割った時の商を"切り上げをここでは行ってます!
(a + b - 1) / b
(len(s) + batchSize -1) / batchSize

a := 10
b := 3

// 通常の除算(切り捨て)
d1 := a / b
fmt.Println(d1) // 3

// 切り上げ除算
d2 := (a + b - 1) / b
fmt.Println(d2) // 4

17. 2つのスライスから同じものを削除

s1 := []int{1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7}
s2 := []int{1, 3, 5, 7}

// 新しいスライスに、bの要素でないaの要素を追加する
a := []int{}
for _, i := range s1 {
    found := false
    for _, j := range s2 {
        if i == j {
            found = true
            break
        }
    }
    if !found {
        a = append(a, i)
    }
}

fmt.Println(a) // [2 4 6 2 4 6]

💡テクいポイント: sliceとmapはどちらが速いのか

  • マップの方がスライスに比べて高速かもしれない。。。🤔 (場合によりけりかも、だけど、、、)
  • この疑問に関して、自分なりの調査をしたので参考はこちら💁 参考

18. スライスの重複排除

// スライスの重複排除: 1
s1 := []int{3, 2, 1, 4, 3, 2, 1, 4, 1}
keys := make(map[int]bool)
result1 := []int{}

for _, entry := range s1 {
    if _, value := keys[entry]; !value {
        keys[entry] = true
        result1 = append(result1, entry)
    }
}

fmt.Println(result1) // [3 2 1 4]

// スライスの重複排除: 2
s2 := []int{3, 2, 1, 4, 3, 2, 1, 4, 1}
sort.Ints(s2)
j := 0
for i := 1; i < len(s2); i++ {
    if s2[j] == s2[i] {
        continue
    }
    j++
    s2[j] = s2[i]
}
result2 := s2[:j+1]
fmt.Println(result2) // [1 2 3 4]

19. メモリリークを防ぐ

メモリリーク
プログラムが動作する上で確保したメモリを適切に解放せず、不要になったメモリ領域がガベージコレクション(不要になったメモリの自動回収)の対象とならない状況です。これにより、プログラムは実際には必要としていないにもかかわらず、次第に増え続けるメモリ領域を占有し続けることになる(大変だ💦)

この結果、システム全体のパフォーマンスが低下したり、最悪の場合、メモリが枯渇してプログラムがクラッシュしたりする可能性があるらしい。

こんな場面の時(メモリリークの例)

// 大きなスライスを作成
largeSlice := make([]int, 1<<20)  // 1MBのスライス

// スライスからサブスライスを作成
subslice := largeSlice[10:20]

// これ以降、subsliceだけを使って処理を行いたい...

何がダメなのか、、、

subsliceはlargeSliceの一部を参照しています。
しかし、その後でlargeSliceが不要になったとしても、subsliceがまだlargeSliceを参照しているため、largeSliceはガベージコレクションの対象にならず、そのメモリは解放されない!!!

対策: 不必要になったデータへの参照を持ち続けないようにする。つまり、新たなスライスにデータをcopyする

// 大きなスライスを作成
largeSlice := make([]int, 1<<20)  // 1MBのスライス

// 新たなスライスにデータをコピー
subslice := make([]int, 10)
copy(subslice, largeSlice[10:20])

// これ以降、subsliceだけを使って処理を行いたい...

subsliceは新たに確保されたメモリ領域にlargeSliceからデータをコピーして作成されています。
そのため、largeSliceが不要になった時点でそのメモリは解放され、メモリリークは発生しない。

💡テクいポイント: sync.Pool

sync.Poolは、頻繁に大きなスライスを作成し、短期間でそのスライスが不要になる場合、オブジェクトの一時的な再利用を可能する!
スライスをプール化することで、メモリアロケーションのコストを削減したいときや、ガベージコレクションの負荷を減らすことができてテクい!!!😏

// Poolを作成します。ここで、新しいオブジェクトを生成するための関数を定義
// この例では、1024の長さを持つint型のスライスを生成
var pool = &sync.Pool{
	New: func() interface{} {
		return make([]int, 1024)
	},
}

func main() {
	for i := 0; i < 10; i++ {

		// Get() Poolからスライスを取得
		s := pool.Get().([]int)

		// 何らかのデータ処理
		fmt.Println(i)
		for j := 0; j < 1024; j++ {
			s[j] = i
		}

		// Put() 使用後はスライスを再利用するためにPoolに戻す
		pool.Put(s)
	}
}

20. スライドウィンドウ

特定の大きさのスライスを、与えられた入力スライスを通じてウィンドウさせた(スライドさせた)結果を返す

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

size := 3

if len(s) <= size {
    fmt.Println([][]int{s})
    return
}

r := make([][]int, 0, len(s)-size+1)

for i, j := 0, size; j <= len(s); i, j = i+1, j+1 {
    r = append(r, s[i:j])
}

fmt.Println(r) // [[3 2 1] [2 1 4] [1 4 3] [4 3 2] [3 2 1] [2 1 4] [1 4 1]]

まとめ

スライスマスターには、まだまだ遠いですが、日々精進します!💪
「ここ違うよ!」、「ここもっとテクい書き方あるよ!」という方は、遠慮なくご指摘いただけると嬉しいです!🙌

参考

 元ネタ

appendcopyについて詳しく知りたい方へ

ガベージコレクション(GC)が知らない方へ(見慣れない言語ですが、すごくわかりやすい)

slicemapはどちらが速いのか

sync.Poolについて

30
21
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
30
21