【Go入門】参照型・スライス – Goの可変長配列

シェアする

こんにちは。Go入門ブログの第14回です。
本記事では、Goにおける代表的な参照型データ構造であるスライスについて解説します。

第8回の記事で、Goの配列型arrayは要素数を変更できない固定長配列であると説明しました。
【Go入門】配列型(要素数までを定義に含める静的配列)

これに対し、スライスはGoにおける可変長配列であると考えることができます。
より柔軟な配列であり、実際には配列型arrayよりも一般に使用されることが多いでしょう。

配列からスライスを生成する

スライスの概念を理解しやすくするため、まず既存の配列からスライスを生成する方法を見てみましょう。
以下の構文で、既存の配列から任意のインデックス範囲を抽出してスライスを生成することができます。

スライス名 = 配列名[インデックス先頭 : インデックス末尾]

実例をみながら解説します。

package main

import "fmt"

func main() {

    // 素数配列を生成
    prime := [6]int{2, 3, 5, 7, 11, 13}

    // 素数配列のインデックス2-4番目の要素を抽出したスライスを生成
    s := prime[2:5]
    fmt.Println(s)
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[5 7 11]

この例では、11行目のs := prime[2:5]でスライスを生成しています。
配列primeから、[2:5]で抽出対象のインデックスを指定しているわけです。
指定範囲を[n:m]とした場合、インデックスのnからm-1が抽出範囲となる点に注意してください。

スライスは配列に対する参照型である

冒頭で、スライスとは参照型であると述べました。
スライスそれ自体にはどんなデータも格納しておらず、元の配列を部分的に参照しているのです。

したがって、スライスの要素の値を更新した場合、元の配列の要素の値にも更新が反映されます。

package main

import "fmt"

func main() {

    // 素数配列を生成
    prime := [6]int{2, 3, 5, 7, 11, 13}

    // 素数配列のインデックス2-4番目の要素を抽出したスライスを生成
    s := prime[2:5]

    // スライスの1番目の要素の値を更新
    s[0] = 0
    fmt.Println(s)
    // 元の配列の値も同様に更新されていることを確認
    fmt.Println(prime)
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[0 7 11]
[2 3 0 7 11 13]

スライスのリテラル

スライスのリテラルは、要素数を指定しない配列リテラルの形で記述できます。
先ほどのコードをベースに、スライスの生成部分をリテラルで記述してみます。

package main

import "fmt"

func main() {

    // 素数配列を生成
    prime := [6]int{2, 3, 5, 7, 11, 13}

    // スライスを生成し初期値をリテラルで記述
    s := []int{prime[2], prime[3], prime[4]}
    fmt.Println(s)

    // スライスの1番目の要素の値を更新
    s[0] = 0
    fmt.Println(s)
    // 元の配列の値が更新されていないことを確認
    fmt.Println(prime)
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[5 7 11]
[0 7 11]
[2 3 5 7 11 13]

11行目がスライスの生成箇所です。
[]int{要素1,要素2,要素3}という書式になっています。
プログラムの挙動としては、要素数が指定された[3]int{要素1,要素2,要素3}という配列を生成し、それを参照するスライスを変数sに代入しているような形となります。

この場合、リテラルの値として配列primeの要素を使用していますが、primeを参照しているわけではないため、スライスの要素を更新しても配列primeには影響がありません。

makeを使用したスライスの定義

既存の配列を使用せずスライスを定義したい場合、組み込み関数makeを使用します。
これは、事前に要素数が定まらない動的配列を定義する方法と考えることができます。

スライス名 := make([]型名, 要素数, 容量)

容量の指定は省略可能です。また、容量の詳細については後述します。
それではサンプルを見てみましょう。

package main

import "fmt"

func main() {

    // 要素数3の数値型スライスを生成
    s := make([]int, 3)
    fmt.Println(s)
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[0 0 0]

各要素の初期値がゼロ値であることに注目してください
make関数はゼロ値の配列を割り当て、その配列を参照するスライスを返しています。

スライスの容量

さて、スライスの容量属性について解説します。
スライスの容量属性とは、そのスライスのためにメモリ上に確保される領域です。

make関数で容量の指定を省略した場合、容量は要素数と同一になります
先の例ではs := make([]int, 3)としており、要素数・容量ともに3です。
つまり、int型の要素を3つ格納できるだけのメモリ領域が、スライスsに割り当てられます。

これに対し、たとえばs := make([]int, 3, 10)とした場合、要素数3に対して容量は10となります。
要素は3つしかないにも関わらず、int型の要素を10個まで格納できるメモリ領域が割り当てらるわけです。
これにはどのようなメリットがあるでしょうか。

先述の通り、スライスとは可変長の配列です。
後ほど説明しますが、後から要素を追加して拡張することが可能です。
この際、先ほどのようにあらかじめ容量を10確保しておけば、要素数が10を越えるまでは新たにメモリ領域を確保する必要が発生しません。

容量を確保していなかった場合は、Goのランタイムは新たに10の容量を確保して、元のスライスが保持していたデータをまるごと新しいメモリ領域へコピーします。
これはなかなかにコストの高い処理であるといえます。

つまり、スライスの容量をあらかじめ指定しておくことは、処理のコストを軽減しパフォーマンスを維持することに貢献します。
スライスの拡張規模が推定可能である場合は、可能な限り容量も指定しておくべきでしょう。

lenとcap

len()関数を使用することで、スライスの持つ要素数を取得することができます。
同様にcap()関数では、スライスの持つ容量を取得することができます。

package main

import "fmt"

func main() {

    // 要素数3の数値型スライスを生成
    s := make([]int, 3, 10)
    fmt.Println(s)
    fmt.Printf("要素数: %d, 容量: %d", len(s), cap(s))
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[0 0 0]
要素数: 3, 容量: 10

要素の追加

スライスに要素を追加するには、組み込み関数appendを使用します。
appendの基本的な使い方は簡単で、第1引数に対象のスライスを指定し、第2引数以降に追加する要素の値を列挙するだけです。

スライス名 = append(スライス名,追加する要素の値1,追加する要素の値2…)

さっそくサンプルをみてみましょう。

package main

import "fmt"

func main() {

    // 要素数3の数値型スライスを生成
    s := make([]int, 3, 10)
    fmt.Println(s)
    fmt.Printf("要素数: %d, 容量: %d \n", len(s), cap(s))

    // 元の容量上限まで要素を追加
    s = append(s, 1, 2, 3, 4, 5, 6, 7)
    fmt.Println(s)
    fmt.Printf("要素数: %d, 容量: %d \n", len(s), cap(s))

    // 元の容量を超える要素追加
    s = append(s, 8)
    fmt.Println(s)
    fmt.Printf("要素数: %d, 容量: %d \n", len(s), cap(s))

}

このサンプルの実行結果は以下のようになります。

$ go run main.go
[0 0 0]
要素数: 3, 容量: 10
[0 0 0 1 2 3 4 5 6 7]
要素数: 10, 容量: 10
[0 0 0 1 2 3 4 5 6 7 8]
要素数: 11, 容量: 20

appendで指定した追加要素が、スライスの末尾へ順番に追加されていることが確認できます。

また、容量の値に注目してください。
ここでは、最初のmakeで元のスライスの容量に10を指定しています。
このため、要素数が10を越えるまでは容量に変動は発生していません。

これに対し、11個目の要素を追加したとき、容量が一気に20まで増えています。
これは先述の通り、元のスライス容量が不足したため、Goのランタイムが自動的にメモリ領域を確保してスライスをコピーした結果です。

自動的に拡張される場合、どの程度の容量が確保されるかはGoのランタイムに依存しますので留意してください。
ここに上げた実行例も筆者の実行環境で得られた結果であり、他の環境で同じ結果になるとは限りません。

スライスに対するループ処理

スライスの実践的な使用方法としては、forによるループ処理を行うことが非常に多いでしょう。
特に、ループの範囲節としてrangeを使用することで、可変長であるスライスの要素数に関わらず、全要素に対して処理を行うことができます。

for、rangeの詳細についてはこちらの記事をご参照ください。

それではサンプルをみてみましょう。
ここでは、上記の記事で使用したコードをスライスに書き換えています。

package main

import "fmt"

func main() {

    // スライスを生成
    capitalArea := []string{"群馬", "栃木", "埼玉", "東京", "神奈川", "千葉", "茨城"}

    // スライスの要素数回ループ
    for index, value := range capitalArea {
        fmt.Printf("首都圏%d: %s\n", index+1, value)
    }
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
首都圏1: 群馬
首都圏2: 栃木
首都圏3: 埼玉
首都圏4: 東京
首都圏5: 神奈川
首都圏6: 千葉
首都圏7: 茨城

固定長の配列と同じように、スライスが持つすべての要素に対して処理を行っていますね。
また、毎回の反復処理において要素のインデックスと値をそれぞれ返していることも確認できます。

もうひとつ、可変長であるスライスならではの例を見てみましょう。
ループ処理の中で要素を追加した場合の挙動の確認です。

package main

import "fmt"

func main() {

    // スライスを生成
    capitalArea := []string{"群馬", "栃木", "埼玉", "東京", "神奈川", "千葉", "茨城"}

    // スライスの要素数回ループ
    for index, value := range capitalArea {
        fmt.Printf("首都圏%d: %s\n", index+1, value)
        // スライスの末尾に要素を追加
        capitalArea = append(capitalArea, "山梨")
    }

    fmt.Println(capitalArea)
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
首都圏1: 群馬
首都圏2: 栃木
首都圏3: 埼玉
首都圏4: 東京
首都圏5: 神奈川
首都圏6: 千葉
首都圏7: 茨城
[群馬 栃木 埼玉 東京 神奈川 千葉 茨城 山梨 山梨 山梨 山梨 山梨 山梨 山梨]

ループ処理中に要素が追加されても、ループの回数には影響を与えず、ループ開始前の要素数だけ反復が行われていることがわかります。
このようにrangeを用いれば、ループ中に要素を追加しても無限ループにはならないので安心です。

終わりに

本記事では、Goにおいて可変長配列の役割を担うスライスについて、基本的な使用方法の解説を行いました。
本文中にて説明したとおり、内部処理的にはスライスと配列型とはまったくことなるものですが、直感的には似たような挙動が得られることがお解りいただけたかと思います。
理解の混乱を避けるために、スライスは配列への参照であるという点は抑えておきましょう。

スポンサーリンク
スポンサーリンク
スポンサーリンク

シェアする

フォローする

スポンサーリンク
スポンサーリンク