yosemite's blog

About technology, books, diary

Go言語の配列とスライスをきちんと理解する

gopher
gopher

はじめに

Go 言語を学習するにあたって、 Slice という便利なものが存在します。
ただ、うまく使い方も理解していないし、配列との違いも説明できないし、実体がどうなっているのかも知らなかったので腰を据えて調べてみました。

引用している説明文、コードについては主に A Tour of Go からの引用です。

Array

まずは、配列についてみていきたいと思います。

[n]T 型は、型 T の n 個の変数の配列( array )を表します。 以下は、intの10個の配列を宣言しています: var a [10]int
配列の長さは、型の一部分です。ですので、配列のサイズを変えることはできません。 これは制約のように思えますが、心配しないでください。 Goは配列を扱うための便利な方法を提供しています。

Array の特徴

  • 宣言方法
    • [3]int{1, 2, 3}
      • サイズを 3 と明記。
    • [...]string{"hoge", "fuga"}
      • サイズは省略できるが、実態としては中身の要素を数えているので、サイズを明記した場合と特に変わりはない。
  • 要素の型と要素のサイズをきちんと指定する
  • 要素のサイズは変更できない
package main

import "fmt"

func main() {
    var a [2]string
    a[0] = "Hello"
    a[1] = "World"
    fmt.Println(a[0], a[1]) // => Hello World
    fmt.Println(a)          // => [Hello World]

    primes := [6]int{2, 3, 5, 7, 11, 13}
    fmt.Println(primes)    // => [2 3 5 7 11 13]
}

Slice

配列をスライスする

Goは配列を扱うための便利な方法を提供しています。

配列で、最後の方にこう書かれていたと思いますが、配列はスライスすることができます。
その方法をみていきます。

下記のように、コロンで区切られた二つのインデックス low と high の境界を指定することによってスライスを形成できます。
a[low : high]
ただし、最後の要素は省かれます。
次の式は 6 つ要素からなる prime という配列の要素のうち 1 から 4 を含むスライスを作ります。

package main

import "fmt"

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}
    s := primes[1:4]
    fmt.Println(s)       // => [3 5 7]
}

「Slice とは Array の参照」である

スライスは配列への参照のようなものです。 スライスはどんなデータも格納しておらず、単に元の配列の部分列を指し示しています。 スライスの要素を変更すると、その元となる配列の対応する要素が変更されます。 同じ元となる配列を共有している他のスライスは、それらの変更が反映されます。

という説明が A Tour of Go に、書いてあります。

package main

import "fmt"

func main() {
    names := [4]string{
        "John",
        "Paul",
        "George",
        "Ringo",
    }
    fmt.Println(names)  // => [John Paul George Ringo]

    a := names[0:2]
    b := names[1:3]
    fmt.Println(a, b)  // => [John Paul] [Paul George]

    b[0] = "XXX"
    fmt.Println(a, b)  // => [John XXX] [XXX George]
    fmt.Println(names)  // => [John XXX George Ringo]
}

上記のように、配列からスライスを形成しました。
では、上のコードより Slice の特徴をまとめてみるとこんな感じではないでしょうか。

Slice の特徴

  • 宣言方法
    • 配列 a に対して a[:]
    • []bool{true, false, true}
  • スライスは配列の参照である(長さのないスライス)
  • 要素を変更すると、その元となる要素も変更される

結局実体はどうなの?

「スライスは配列への参照」という意味が最初はちょっと分かり辛かったのですが、下記の記事がすごく分かりやすく解説してありました。
ぜひ参考にしていただけると理解しやすいかと思います。

qiita.com

実際はどうようなデータ構造になっているのか、実体は何なのかそれぞれをコードで書きながら一歩踏み入れて考えていきます。

配列の実体

package main

import "fmt"

func main() {
    hoge := []int{2, 4, 6, 8}
    fuga := hoge
 
    fmt.Println(hoge)  // => [2 4 6 8]
    fmt.Println(fuga)  // => [2 4 6 8]
    
    fmt.Println(&hoge[0])  // => 0x414020
    fmt.Println(&fuga[0])   // => 0x414030
}

スライスの実体

package main

import "fmt"

func main() {
    hoge := []int{2, 4, 6, 8}
    fuga := hoge
 
    fmt.Println(hoge)  // => [2 4 6 8]
    fmt.Println(fuga)  // => [2 4 6 8]
    
      fmt.Println(&hoge[0])  // => 0x414020
    fmt.Println(&fuga[0])   // => 0x414020
}

それぞれのアドレスを & で出力しているところをみると違いが明らかだと思います。
まとめると配列とスライスの違いは下記のようになります。

  • 配列
    • 変数ごとにアドレスが異なる
      • それぞれが別のメモリを占有する
      • 一方を変更しても、もう一方に影響はない
  • スライス
    • 変数ごとにアドレスが同じ
      • 内部で配列が生成されている
      • スライス自体はメモリを占有しない(生成された配列のアドレスを参照しているに過ぎない)
      • 一方を変更したら、もう一方にも影響が生じる

おわりに

今回は、基本中の基本である、配列とスライスについて学習してみました。
僕自身も Go を書いている人の中「スライスだけで良くね?」という派でしたが、配列を知ることでスライスへの理解が深まった気がします。
次回はスライスの実用的な使い方(要素数を数える、追加、削除…)を学習できればと思います。

最後まで見てくださりありがとうございました。

それじゃまた🙋‍♂️