[Golang] Slice

2021-11-09 hit count image

GolangでSlice(スライス)について説明して、使う方法に関しても説明します。

概要

今回のブログポストではGolangのSlice(スライス)について、詳しく説明して使う方法も説明します。このブログポストで紹介するコードは次のリンクで確認できます。

Slice

スライスはGolangが提供する動的配列タイプ(配列を指定するポインタタイプ)です。

  • 性的(Static): コンパイル時点(Compile)で決定
  • 動的(Dynamic): 実行時点(Runtime)で決定

次はGolangで配列を宣言する方法です。

var v [10]int

スライスは次のように配列のサイズを設定しなくて宣言します。

var v []int

これを確認するためmain.goファイルを生成して次のように修正します。

package main

import "fmt"

func main() {
  var a [2]string
  var b []string

  fmt.Println(a)
  fmt.Println(b)
}

これを実行すると次のような結果が表示されます。

# go run main.go
[ ]
[]

配列とスライス

スライスは配列を指定するポインタタイプです。これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

func changeArr(arr2 [5]int) {
  arr2[0] = 100
}
func changeSlice(slice2 []int) {
  slice2[0] = 100
}

func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := []int{1, 2, 3, 4, 5}

  changeArr(arr)
  changeSlice(slice)

  fmt.Println(arr)
  fmt.Println(slice)
}

これを実行すると下記のような結果が表示されます。

# go run main.go
[1 2 3 4 5]
[100 2 3 4 5]

配列はポインタタイプではないので、changeArr関数をコールする時、arr2と言う新しいインスタンスが生成れて、arrの内容がコピーされます。したがって、changeArr関数名kでarr2の値を変更してもarrの値は変更されないです。

しかし、スライスはポインタタイプです。したがって、changeSlice関数をコールすると、slice2と言う新しいインスタンスを生成するではなく、sliceのメモリアドレスをコピーすることになります。したがって、changeSlice関数中でslice2の値を変更するとsliceの値も変更されます。

lenとcap

スライスは配列とは違ってlen以外にもcapと言うデータを持っています。スライスのlenは現在スライスで使っているサイズを意味して、capは現在スライスの総サイズ(使ってるサイズ+開いてるサイズ)を意味します。

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

func main() {
  slice1 := []int{1, 2, 3, 4, 5}
  slice2 := make([]int, 2, 10)

  fmt.Printf("slice1(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)
}

これを実行すると下記のような結果が表示されます。

# go run main.go
slice1(0xc0000b2000): len=5 cap=5 [1 2 3 4 5]
slice2(0xc0000b4000): len=2 cap=10 [0 0]

make関数

Golangで提供するmake関数を使ってスライスを生成することが出来ます。

slice1 := make([]int, 10)
fmt.Println(slice1)

上のようにmake関数を使って10のサイズを持ってるスライスを生成することが出来ます。

slice2 := make([]int, 2, 10)
fmt.Println(slice2)

または、上のようにmake関数を使って10のサイズを持ってるスライスを生成して2つの要素だけ使えるようにすることが出来ます。

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

func main() {
  slice1 := make([]int, 10)
  fmt.Println(slice1)

  slice2 := make([]int, 2, 10)
  fmt.Println(slice2)
}

これを実行すると次のような結果が表示されます。

# go run main.go
[0 0 0 0 0 0 0 0 0 0]
[0 0]

スライシング

配列の一部を取り出してスライスを作ることをスライシング(Slicing)と言います。

  • Array => Slicing => Slice

Golangでスライシングは次のように使えます。

Array[startIndex:endIndex]

startIndexからendIndexの直前(endIndex -1)までの値をリターンします。この時、リターンされたSliceのcapはstartIndexからArrayの最後の最後までのサイズです。

これを確認するためmain.goファイルを次のように修正します。

package main

import "fmt"

func main() {
  array := [5]int{1, 2, 3, 4, 5}
  slice := array[1:2]

  fmt.Printf("array: len=%d %v\n", len(array), array)
  fmt.Printf("slice: len=%d cap=%d %v\n", len(slice), cap(slice), slice)
}

これを実行すると次のような結果が表示されます。

# go run main.go
array: len=5 [1 2 3 4 5]
slice: len=1 cap=4 [2]

スライスをスライシングしてスライスを生成することが出来ます。また、次のようにstartIndexに0を指定すると最初からスライシングをすることが出来ます。ここで0は省略することが出来ます。

slice = []int{1, 2, 3, 4, 5}
slice1 := slice[0:3]
slice2 := slice[:3]

次のように使う場合、最後までスライシングをすることが出来ます。ここで最後のindex省略することが出来ます。

slice = []int{1, 2, 3, 4, 5}
slice1 = slice[2:len(slice)]
slice2 = slice[2:]

最後に、次のように全てをスライシングすることが出来ます。これは配列をスライスに変換する時よく使えます。

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

スライシングは新しい変数を作るのではなく、単純にメモリアドレスを指定するだけなので次のように使うことが出来ます。

array1 := [100]int{1: 1, 2: 2, 99: 100}
slice1 = array1[1:10]
slice2 = slice1[2:99]

fmt.Println(slice1)
fmt.Println(slice2)

上の例題ようにslice1array1のメモリアドレスを指定してますので、slice1で99までの値を取り出してslice2を作ることが出来ます。

capサイズを調整してスライシング

Arrayをスライシングすると、リターンされたSliceのcapはstartIndexからArrayの最期までのサイズです。しかし、次のようにスライシングする時、maxIndexを追加してcapのサイズを調整することが出来ます。

slice[startIndex:endIndex:maxIndex]
slice1 = []int{1, 2, 3, 4, 5}
slice2 = slice1[1:3:4]

fmt.Printf("slice1: len=%d cap=%d %v\n", len(slice1), cap(slice1), slice1)
fmt.Printf("slice2: len=%d cap=%d %v\n", len(slice2), cap(slice2), sl

append関数

スライスの最後に要素を追加するためにはappend関数を使う必要があります。append関数を使ってスライスに要素を追加する場合、新しい要素が追加されたスライスがリターンされます。

var slice1 = []int{1, 2, 3}
slice2 := append(slice1, 4)

この時、新しいスライスは既存のスライスと同じメモリアドレスを使う時も、同じメモリアドレスを使わない場合もあります。

appendを使って新しい要素をスライスに追加する時、スライスに新しい要素を追加する空間がある場合、新しいスライスは既存のスライスのメモリアドレスを使うことになります。しかし、既存スライスに空間が足りない場合、appendは新しいメモリアドレスに既存スライスをコピーした後、新しい要素を追加することになります。

これを確認するため、main.goファイルを次のように修正します。

package main

import "fmt"

func main() {
  slice1 := make([]int, 3)
  slice2 := append(slice1, 4)

  fmt.Println("[New splice]")
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  fmt.Println("slice1 changed ========================================================")
  slice1[0] = 100
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  fmt.Println("slice2 changed ========================================================")
  slice2[0] = 200
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  slice1 = make([]int, 1, 3)
  slice2 = append(slice1, 4)

  fmt.Println("[Same slice]")
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  fmt.Println("slice1 changed ========================================================")
  slice1[0] = 100
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  fmt.Println("slice2 changed ========================================================")
  slice2[0] = 200
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)
}

これを実行すると次のような結果が確認できます。

# go run main.go
[New splice]
slice(0xc00012a000): len=3 cap=3 [0 0 0]
slice2(0xc00012c000): len=4 cap=6 [0 0 0 4]
slice1 changed ========================================================
slice(0xc00012a000): len=3 cap=3 [100 0 0]
slice2(0xc00012c000): len=4 cap=6 [0 0 0 4]
slice2 changed ========================================================
slice(0xc00012a000): len=3 cap=3 [100 0 0]
slice2(0xc00012c000): len=4 cap=6 [200 0 0 4]
[Same slice]
slice(0xc00012a018): len=1 cap=3 [0]
slice2(0xc00012a018): len=2 cap=3 [0 4]
slice1 changed ========================================================
slice(0xc00012a018): len=1 cap=3 [100]
slice2(0xc00012a018): len=2 cap=3 [100 4]
slice2 changed ========================================================
slice(0xc00012a018): len=1 cap=3 [200]
slice2(0xc00012a018): len=2 cap=3 [200 4]

スライスのcapに新しい要素を追加する余裕がある場合、同じメモリアドレスを使うことが確認できます。したがって、スライスのappendを使う時には、既存のスライスと新しいスライスが同時に変更されることを気をつける必要があります。

スライスのコピー

次のようにスライシングを使ってスライスをコピーすることが出来ます。

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

slice2[1] = 100

fmt.Println(slice1)
fmt.Println(slice2)

しかし、スライシングは新しいスライスを生成することではなくメモリアドレスを共有することなので、上のようにslice2の値を変更するとslice1の値も変更されます。

この問題を解決するためには次のようにslice1と同じサイズあのslice2を作って、ループで値をコピーします。

slice1 = []int{1, 2, 3, 4, 5}
slice2 = make([]int, len(slice1))

for i, v := range slice1 {
  slice2[i] = v
}

slice2[1] = 100

fmt.Println(slice1)
fmt.Println(slice2)

または、次のようにappend関数を使って新しいスライスを作って、全ての内容を追加してコピーすることも出来ます。

slice1 = []int{1, 2, 3, 4, 5}
slice2 = append([]int{}, slice1...)

slice2[1] = 100

fmt.Println(slice1)
fmt.Println(slice2)

最後に、make関数を作って同じサイズのスライスを生成した後、copy関数を使って全ての値をコピーすることも出来ます。

slice1 = []int{1, 2, 3, 4, 5}
slice2 = make([]int, len(slice1))

copy(slice2, slice1)

slice2[1] = 100

fmt.Println(slice1)
fmt.Println(slice2)

Golangでcopy関数は次のように使うことが出来ます。

copy(dst, src)

削除

次のようにスライスで特定要素を削除することが出来ます。

slice := []int{1, 2, 3, 4, 5, 6}
deleteIdx := 2

fmt.Println(slice)
for i := deleteIdx + 1; i < len(slice); i++ {
  slice[i-1] = slice[i]
}
slice = slice[:len(slice)-1]
fmt.Println(slice)

または、次のようにappendを使って削除することもできます。

slice = []int{1, 2, 3, 4, 5, 6}
deleteIdx = 2

fmt.Println(slice)
slice = append(slice[:deleteIdx], slice[deleteIdx+1:]...)
fmt.Println(slice)

最後に、copy関数を使って削除することもできます。

slice = []int{1, 2, 3, 4, 5, 6}
deleteIdx = 2

fmt.Println(slice)
copy(slice[deleteIdx:], slice[deleteIdx+1:])
slice = slice[:len(slice)-1]
fmt.Println(slice)

要素追加

Golangでは次のようにforループを使ってスライスに新しい要素を追加することが出来ます。

slice := []int{1, 2, 3, 4, 5, 6}
insertIdx := 2

fmt.Println(slice)
slice = append(slice, 0)
for i := len(slice) - 2; i >= insertIdx; i-- {
  slice[i+1] = slice[i]
}
slice[insertIdx] = 100
fmt.Println(slice)

または、次のようにappendを使ってスライスに新しい要素を追加することもできます。

slice = []int{1, 2, 3, 4, 5, 6}
insertIdx = 2

fmt.Println(slice)
slice = append(slice[:insertIdx], append([]int{100}, slice[insertIdx:]...)...)
fmt.Println(slice)

最後に、copy関数を使ってスライスに新しい要素を追加することもできます。

slice = []int{1, 2, 3, 4, 5, 6}
insertIdx = 2

fmt.Println(slice)
slice = append(slice, 0)
copy(slice[insertIdx+1:], slice[insertIdx:])
slice[insertIdx] = 100
fmt.Println(slice)

スライスソーティング

Golangでは基本的提供されるsortパッケージを使って、スライスをソーティングすることが出来ます。

package main

import (
  "fmt"
  "sort"
)

func main() {
  slice := []int{6, 3, 1, 5, 4, 2}

  fmt.Println(slice)
  sort.Ints(slice)
  fmt.Println(slice)
}

次のように構造体スライスもsortを使ってソーティングすることが出来ます。

package main

import (
  "fmt"
  "sort"
)

type Student struct {
  Name string
  Age  int
}

type Students []Student

func (s Students) Len() int           { return len(s) }
func (s Students) Less(i, j int) bool { return s[i].Age < s[j].Age }
func (s Students) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

func main() {
  students := []Student{
    {"c", 31},
    {"a", 20},
    {"b", 21},
    {"d", 19},
  }

  fmt.Println(students)
  sort.Sort(Students(students))
  fmt.Println(students)
}

完了

これでGolangでスライスを宣言して使う方法についてみてみました。また、配列とスライスの違い、スライシングを使って配列からスライスを作る方法も見てみました。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

Posts