2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoのArrayとSliceについて深掘りしてみた

Last updated at Posted at 2024-09-24

株式会社Schooの @hiroto_0411です!
Go研修を受けた際に、「開発だとSliceを使うことが多いです。」と言われており、ArrayとSliceの違いに興味を持ったので深掘りしてみました!

簡単にまとめると

  • Arrayの全てまたは一部を参照しているのがSlice

  • 柔軟性の高いSliceがよく使われる

  • Sliceの作成方法は使い分けられると良い

Array

func main(){
    var a [10]int
    fmt.Println(a)  //[0 0 0 0 0 0 0 0 0 0]
    
    a[0] = 12
    a[2] = 17
    
    fmt.Println(a)  //[12 0 17 0 0 0 0 0 0 0]
    
    b := [6]int{1, 2, 3, 4, 5, 6}
    fmt.Println(b) //[1 2 3 4 5 6]
}
  • Arrayの長さ(要素数)まで型の一部である([10]intで一つの型)

  • 要素数は変更できない

Slice

Sliceの特徴

  • Sliceは要素数が可変である
  • Sliceを作成したら、裏では配列が作成されている
  • Sliceは作成された配列のポインタ、セグメントの長さ(length)、セグメントの容量(capacity)を持っている

参考

色々なSliceの作成方法

空のSliceを作成

func main(){
	var s []int
	fmt.Printf("len=%v cap=%v %v\n", len(s), cap(s), s)
	//len=0 cap=0 []
}
  • 長さと容量はどちらも0

  • nilとなる

Slice literal

func main(){
	var s []int = []int{1, 2}
	fmt.Println(s) //[1 2]
}
  • 初期値を設定してSliceを作りたい場合はSlice literalが有効

makeを使う方法

func main() {
	s := make([]int, 5, 10)
	fmt.Printf("len=%v cap=%v %v\n", len(s), cap(s), s) //len=5 cap=10 [0 0 0 0 0]
}
  • 長さと容量を指定してスライスを初期化できる
  • make関数はゼロの配列を確保し、その配列を参照するスライスを返している
  • 初期値を設定することはできない
  • 容量を事前に確保することができるので、リアルタイム処理や、パフォーマンスが重要視される処理など無駄なallocateを防ぎたいときはmakeが有効

※allocateについては下で説明しています

Arrayの全部または一部を参照しているのがSlice

func main() {
	b := [6]int{1, 2, 3, 4, 5, 6}
	fmt.Println(b) //[1 2 3 4 5 6]
	
	//スライスの作成(配列からSliceを作る方法)
	c := b[0:3]

	fmt.Println(c) //[1 2 3]
	
	//要素に別の値を入れる
	c[0] = 100

    //b配列の1つ目の要素のaddressとc配列の1つ目の要素のaddressは同じになっている
    fmt.Printf("Address of b[0]: %v\n", unsafe.Pointer(&b[0])) //Address of b[0]: 0x14000018150
	fmt.Printf("Address of c[0]: %v\n", unsafe.Pointer(&c[0])) //Address of c[0]: 0x14000018150

     //スライスcはbの配列を参照しているため、どちらの値も100になっている
	fmt.Println(c) //[100 2 3]
	fmt.Println(b) //[100 2 3 4 5 6]
}
  • Sliceはデータを保持しておらず、配列の一部または全部を参照している
    そのため、スライスの要素を変更すると配列の対応する箇所の要素も変更される

Sliceのlengthとcapacity

length

長さ
Sliceに含まれる要素の数

func main() {

	b := [6]int{1, 2, 3, 4, 5, 6}
	fmt.Printf("len=%v %v\n", len(b), b) //len=6 [1 2 3 4 5 6]

	c := b[0:3]
	fmt.Printf("len=%v %v\n", len(c), c) //len=3 [1 2 3]

}

capacity

容量
スライスの開始位置から参照先の配列の末尾までの要素数がcapacityになる

func main() {

	b := [6]int{1, 2, 3, 4, 5, 6}
	fmt.Printf("cap=%v %v\n", cap(b), b) //cap=6 [1 2 3 4 5 6]

	c := b[0:1]
	fmt.Printf("cap=%v %v\n", cap(c), c) //cap=6 [1]

	d := b[3:5]
	fmt.Printf("cap=%v %v\n", cap(d), d) //cap=3 [4 5]
}

Sliceへ要素を追加する

func main() {
	var s []int
	fmt.Printf("cap=%v %v\n", cap(s), s) //cap=0 []
	
	//追加
	s = append(s, 1)
	fmt.Printf("cap=%v %v\n", cap(s), s) //cap=1 [1]
}

Sliceの容量を超えると新しい配列が確保される(allocation)

func main() {
	b := []int{1, 2, 3, 4, 5, 6}
	fmt.Printf("cap=%d %v\n", cap(b), b) //cap=6 [1 2 3 4 5 6]
	fmt.Printf("Address of b[0]: %v\n", unsafe.Pointer(&b[0])) //Address of b[0]: 0x140000b0000

	c := b[0:6]
	fmt.Printf("cap=%d %v\n", cap(c), c) //cap=6 [1 2 3 4 5 6]
	fmt.Printf("Address of c[0]: %v\n", unsafe.Pointer(&c[0])) //Address of c[0]: 0x140000b0000

	// 要素を追加して、Slice c の容量を超えるとaddressが変わっているのがわかる
	c = append(c, 7)
	fmt.Printf("cap=%d %v\n", cap(c), c) //cap=12 [1 2 3 4 5 6 7]
	fmt.Printf("Address of c[0]: %v\n", unsafe.Pointer(&c[0])) //Address of c[0]: 0x1400008c060
}

スライスに対して append を使用して元の配列の容量を超えると、Goランタイムは容量を増やした新しい配列を確保し、元の配列の内容と追加された要素をその新しい配列にコピーする。その結果、スライスは元の配列に影響を与えることなく拡張される。この動作により、元の配列 b は変更されず、スライス c は新しい配列を指すようになる。

allocationの動作については以下の記事を見つけた。
容量が1000近くまでは容量を倍にして新しい配列を確保し、それ以降は増加率が低くなると書かれている。

参考

参考文献


Schooでは一緒に働く仲間を募集しています!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?