Go言語のキャッチアップ その2 ~配列~

Goの配列(Array)

Goの配列は、同じ型の値を決まった個数だけ格納するデータ構造。 RubyのArrayと似ているが、重要な違いがある。

Goの配列とは

たとえば、整数を3つ格納する配列を作る。

package main

import "fmt"

func main() {
	numbers := [3]int{10, 20, 30}
	fmt.Println(numbers)
	// [10, 20, 30]
}

この[3]intの意味はこんな感じ。

  • 3: 要素数
  • int: 各要素の型
  • {10, 20, 30}: 格納する値

この3つの値は、3つのint型の値が連続したメモリ領域に格納される。

ちなみにRubyならこんな感じ。

numbers = [10, 20, 30]

なんと簡単な書き方。 しかしRubyの配列は要素を自由に追加・削除できるため、どこかのタイミングで意図せず値が追加されていたり、削除されていたりする場合もある。

Goの配列は違う。

Goの配列は要素数を変更できない

package main

func main() {
	numbers := [3]int{10, 20, 30}

	numbers[0] = 100 // OK

	// 4つ目の要素を追加することはできない。
	numbers[3] = 40
}
// ./main.go:9:10: invalid argument: index 3 out of bounds [0:3]

つまり、要素の値は変更できるが、要素数は変更できない。 また、Goでは配列の長さも型の一部となる。

package main

import (
	"fmt"
	"reflect"
)

func main() {
	a := [3]int{10, 20, 30}
	b := [4]int{10, 20, 30, 40}

	fmt.Println(reflect.TypeOf(a))
	// [3]int
	fmt.Println(reflect.TypeOf(b))
	// [4]int
}

こんな感じでaとbは異なる型になる。 (※reflect.TypeOf()は型をチェックする関数) これはRubyにはない特徴。

配列とメモリの関係

Goの配列は、要素がメモリ上に連続して配置される。 たとえば、intが8バイトの環境なら、以下のようになる。 配列のメモリ配置イメージ このように、それぞれの値が連続した領域に格納される。

Goの配列は代入するとコピーされる

Rubyの場合

a = [10, 20, 30]
b = a

b[0] = 999

p a # [999, 20, 30]
p b # [999, 20, 30]

このようにaとbが同じ配列オブジェクトを参照する。

一方Goの場合

package main

import "fmt"

func main() {
	a := [3]int{10, 20, 30}
	b := a

	b[0] = 999
	fmt.Println(a)
	// [10 20 30]
	fmt.Println(b)
	// [999 20 30]
}

Goでは配列を別の変数に代入すると、配列全体の値がコピーされる。

関数の引数に配列を渡す場合もコピーされる

前述の通り、Goでは配列を別の変数に代入すると配列全体がコピーされる。 これは、関数の引数として配列を渡した場合も同様の事が起こる。

package main

import "fmt"

func update(numbers [3]int) {
    numbers[0] = 999
    fmt.Println(numbers)
    // [999 20 30]
}

func main() {
    numbers := [3]int{10, 20, 30}

    update(numbers)
   
    fmt.Println(numbers)
    // [10 20 30]
}

update関数の中では、配列の先頭要素を999に変更している。 しかしmain関数で出力すると元の値のままになっている。 これは関数に配列を渡した際に、配列全体の値がコピーされているため。 つまり、update関数が変更しているのは元の配列ではなくコピーされた配列ということになる。

なぜGoでは配列をコピーするのか?

Goの配列は値型であり、代入や関数への引数渡しでは配列全体の値がコピーあsれる。 そのため、コピー先の配列を変更しても、元の配列には影響しない。

ただし、大きな配列の場合はコピーするデータ量も増えるため、実際のGoの開発では、より柔軟に扱えるスライスがよく使われる(らしい) 次回はスライス!