Go 言語のスライスの使い方、仕組みを理解する-その1

はじめに

前回のエントリ でスライスが登場したので、理解を整理しました。

Go 言語における スライス ( Slices )  型は順番に並んだデータを取り扱う場合に便利です。
スライス は他言語に用意されている 配列 ( Arrays ) に似ていますが、少し変わった特徴を持っています。

スライス とは何なのか、 スライス とはどのように使われるのかを見ていきます。

※今回は触りにとどめ、次回でスライスの内部はどの様になっているのか ?を掘り下げてみていきます

配列 ( Arrays )

スライス型は Go の配列型をベースとした機能になります。
そのため、スライスを理解するためには配列を理解する必要があります。

配列型の定義には、 「長さ」「型」 の指定が必要です。
例えば、 [4]int という型は int 型(整数型)の要素を 4 つ持つ配列を表します。

配列の型は、サイズを含めて個別です。 [4]int型と[5]int型は、int なので一緒に見えますが、両者は別の です。

配列の各要素の値を参照したい場合には、他のプログラミング言語同様 a[0] のように指定します。
( ここでは、 a という変数に格納されている一番最初の要素を参照する例を表しています。 )

// 4つの要素を持つint型配列を作成
var a [4]int

// 最初の要素に1を代入
a[0] = 1

// 変数iに最初の要素を設定
i := a[0]

// このタイミングで i == 1 となっている

// 要素数の異なる配列を作成
var b [5]int

// b = a はできない。 a = b できない。

int 型配列を初期化しなかった場合は、 0 で初期化されます。

// a[2] == 0

[4]int 型の情報は、メモリ上には連続した 4 つの int 型の値として配置されます。

変数 a の中身は以下のようになっています。

| 1 | 0 | 0 | 0 |

Go の配列の実体は、メモリ上に連続して並んだ値です。

( C 言語のような ) 最初の配列要素へのポインタを指し示したものではありません。

ですから、配列変数を別の配列変数に代入したり、配列変数の値を書き換えたりした場合には、コピーが作成されます。

以下の 2 つのコードのいずれも、 [2]string 型の配列を宣言と同時に初期化するためのリテラル表現です。

// One way
b := [2]string{"Penn", "Teller"}

// Another way
c := [...]string{"Penn", "Teller"}

スライス ( Slices )

ここまで「配列」について見てきましたが、実際 Go のコードではあまり見ることはありません。

殆どの場合、配列をベースにした機能であり非常に強力で便利な スライス を使うためです。

スライス型を宣言する

スライス型を指定するには []T のように記述します。 T の部分には、スライスに格納する型を指定します。
配列型の指定と非常によく似ていますが、 長さを指定しない 点に違いがあります。

スライス変数の初期化

スライス型の変数を初期化する「リテラル表現」も配列型の変数のときと似ています。
やはり長さを指定しません。

letters := []string{"a", "b", "c", "d"}

make 組み込み関数を使ってもスライス型変数を生成できます。

リテラル表記では書ききれないようなサイズの場合に便利です。

func make([]T, len, cap) []T

T の部分には、スライスに格納する要素の型が入ります。
make 関数が呼び出されると、配列をメモリ上に用意し、その配列を参照する スライス が返却されます。

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

第 3 引数の capacity が省略された場合は、第 2 引数の length と同じ値が指定されたものとして扱われます。

先程のコードは以下のように書くこともできます。

s := make([]byte, 5)

スライスの length ( 長さ ) と capacity ( 容量 ) を確認する

スライスの length(長さ)capacity(容量) は組み込み関数である len 関数、 cap 関数を利用して確認できます。

lengthcapacity とはなにか、については次回のエントリ で解説します。

len(s) == 5
cap(s) == 5

スライスの初期値

  • スライスの初期値は nil です。
  • len 関数、 cap 関数の適用結果は両方とも 0 を返します。

package main

import (
    "fmt"
)

func main() {
    var s []byte

    if s == nil {
        fmt.Println("nil")
    }

    fmt.Println(s)
    fmt.Println(len(s))
    fmt.Println(cap(s))
}

$ go run ./main.go
nil
[]
0
0

スライスを「分割」する

スライスや配列を 「分割」 して新たなスライスを生成することができます。

コロン : の両脇を数字で挟んで範囲を指定します。 : の左には分割の 開始位置: の右には分割の 終了位置 を指定します。
要素を参照する方法 ( ex: a[1] ) に似ていますね。

「分割の開始位置」、「分割の終了位置」というのが少しわかりにくいですが、スライスの各要素の間にある境目の位置のことです。

例えば以下のコードで s[1:4] というスライスの分割表現を考えてみます。

s := []string{"g", "o", "l", "a", "n", "g"}

// s[1:4] == []string{"o", "l", "a"}

これは以下のように理解できます。

// 境界1から境界4の間の要素を抽出
0   1---2---3---4   5   6
| g | o | l | a | n | g |

コロンの左の数値、右の数値を省略した場合には、それぞれ スライスの先頭スライスの末尾 までを指定した扱いになります。

package main

import (
    "fmt"
)

func main() {
    s := []string{"g", "o", "l", "a", "n", "g"}

    fmt.Printf("%q\n", b[1:4])
    fmt.Printf("%q\n", b[:2])     // == s[0:2]
    fmt.Printf("%q\n", b[2:])     // == s[2:6]
    fmt.Printf("%q\n", b[:])      // == s[0:6]
}

実行結果は以下のようになります。

$ go run ./main.go
["o" "l" "a"]
["g" "o"]
["l" "a" "n" "g"]
["g" "o" "l" "a" "n" "g"]

ひとこと

今日はここまで。
今回は触りにとどめました。

次回でスライスの内部はどの様になっているのか? を掘り下げてみていきます

Posted by genzouw