MasaのITC Life

夢は起業家!全てにおいて凡人だけど頑張ることだけはいっちょ前!

プログラミング

Go言語のスライスのあれこれを超徹底的に調べてみた!?

投稿日:



以下であげる疑問で、

一つでも分からない点があったら

この記事は少しでも役に立つと思います。



Goで私がつまずいた点も含め、

一から順に解説をしてあります。



まずは簡単なものから順に、

リストアップしてます。




「初学者向き」

  • スライスと配列の違い
  • スライスの作成法
  • スライスとメモリ
  • スライスの初期化方法
  • スライスへの代入
  • スライスのコピー:copy()
  • append()の使い方




「脱」初学者向き

  • コピーしたスライスのコピー元を変更したら、コピー先も変更は反映されるのか
  • copy()と「=」はどう違うのか
  • nilスライスとは
  • Non-nilスライスとempty
  • append()とメモリ
  • append()の応用
  • スライスを含む構造体



スライスは以下の構成をとっています。

type SliceHeader struct {
  Data uintptr
  Len int
  Cap int
}


参照する配列のアドレスと

その長さ、容量、

これら3つをまとめたものです。

図で表すとこんな感じです。

参照:https://blog.golang.org/slices-intro



ただ「配列のポインタ」

というわけではありません。

長さや容量の要素も含めたものを言います。



「長さ」という要素をもつため、

スライスとは「可変長配列」

という表現も出来ます。



なおスライスを宣言すると、

ポインタと長さそして容量

これら3つを宣言したことになります。



スライスを宣言しただけでは使えないので、

スライスの参照先である配列も

しっかりと定義しする必要があります。



宣言したスライスは、

後からでも配列の長さを変えられる

という故に "可変長" (スライス)です。




c言語でいうとことの "配列" は、

固定長配列であり、

スライスではなくGo言語でも

単に配列という表現を使っています。




はじめてスライスの説明を聞く方だと、

頭がこんがらがってくる可能性があるので、

根本的に理解するためにも、

メモリの動向も把握していると良いでしょう。




では、代入の基本的なところから

メモリの話まで見ていきましょう!




以下のパッケージを

importしておきます。


なお、スライスにそれぞれ

"s + 数字" で割り当てています。

(例えば、s1, s2, s3のように)



伝えたいことが変わるたびに

新しくスライスを作成しています。

よって記事全体を通して、

スライスは通し番号になっています。


スライスの作成法と代入法


まずはこれから、

実際のプログラムでスライスを使うには、

どのように準備をしたら良いのか話します



まずはスライス作成についてです。

細かく分けて3つの状態があります。


slice1 : nilスライス
slice2 : Non-nil & empty スライス
slice3:Non-nil & Non-empty スライス


※この表現が正しいかわかりませんが、
 当記事では上記のように表現しています。



Slice1:

nilとは実態がないことです。

スライスを宣言しただけの状態です。

なのでスライス自身のアドレスは、

メモリに確保されますが、

参照先である配列は存在しません。

上の実行をご覧いただく通り、

配列のアドレスはどこもさしておりません。



slice2:

Non-nilとはnilではなく、

実態は一応あるスライスです。

参照先である配列のアドレスは

存在しますが、値は入っておりません(空)。




slice3:

実態もあり、空でもないスライスです。

値は0が格納さています。




以下がソースになります。

ソースにも説明を加えてありますので、

一度ご覧になってみて下さい。



以上でスライスの用意が終わったので、

次はスライスの値を操作していきます。


スライスが参照する配列への代入法



スライスに代入していきます。

厳密には「スライスが参照する配列に」

ということです。




数パターンの書き方をご紹介します。


スライスに代入するには、

配列のアドレスが存在する必要があります。

厳密に定義するなら、

Non-nilである必要があります。



nil状態のスライスに代入すると

例えば以下のエラーが起こります。

goroutine 1 [running]:
main.main()
C:/Users/user/slice.go:58 +0xaed
exit status 2



スライスの長さは気にしなくても、

代入程度であれば、出来ます。



一度に代入する場合は、make()で、

事前に初期化してなくてもOKです。

長さや容量は、コンパイラが数えて

自動でスライスに格納してくれます。



要素一個一個に代入していく場合は、

Len(長さ)以上の要素には代入出来ません。

なのでこの場合は事前に、make()で

必要な要素を用意しておく必要があります。




後で紹介するコピーする際には、

スライスの長さも必要になってきますので、

事前にmake()しておく必要があります。



前述した通りs5, 6 のように

一度に代入してもちゃんと動作します。

場合によりますが基本的には、

一度に代入出来る方が便利ですよね。



それじゃ要素一個一個に代入するのは

「バカみたいじゃん!?」

と思わるかもしれませんが、

一度に代入する方法にも罠があります。



普通にスライスを使う分には、

気にする必要がありませんが、

実は配列のアドレスが変わります。

つまり配列が作り直されることになります。



要素一個に代入する場合は、

配列があるアドレスは変わりません。


※スライスのアドレスではなく、
スライスが参照する配列のことです。




メモリの話が絡んできて、

少し長くなってしまうので、

詳しいことは他の記事にてまとめます。



記事の最後の方にも、

メモリとスライスに関して、

もう一度触れています。



さて話し戻しまして、

slice5, 6 とslice7は似ています。



事前にスライスだけを

作成しておく場合はs5, 6を

作成と同時に代入まで行う場合は、

s7の書き方が普通だと思います。




なおC言語において、

配列に文字列を代入すると

最後の要素にはNULLが入り

要素数は文字数+1になります。

しかしGo言語は文字数のままです。




それではスライスの値を

もう少し操作してみましょう。


スライスのコピー方法とTips


コピーをするには、

組み込み関数であるcopy()を使います。



以下のソースでは、

コピー元のスライスとして、

s8 := []int{1, 2, 3}を用意します。



コピー先として、

s9, s10, s11を用意します。

それぞれ3つの状態に分けます。



s9 : nilスライス
s10 : Non-nil & empty スライス
s11:Non-nil & Non-empty スライス


どのスライスであれば、

コピーできるのか見ていきます。



ソースです。



実行結果はソース内にも

記載してありますが、

以下のようになります。


s9:[]
s10:[]
s11:[1 2 3]



つまりこのことから分かるのが、

スライスを別のスライスに

コピーしたいと思ったら事前に、

make()にてスライスが参照する

配列の箱を用意する必要があります。




もう少しコピーについて

踏み込んでいきます。



もしスライスをコピーするときに、

copy()ではなく「=」を使ったら

何がおこるのでしょうか。



わざわざcopy()関数が用意されているため

イコールとは違うはずです。



コピー先に新たに s12 を作ります。

まずは出来るかどうかの結果から!


ということでcopy()を使わずとも、

スライスのコピーは出来ました。

それではcopy()と「=」とでは、

何が違うのでしょうか。



メモリも絡めて見てみましょう。


s8:コピー元
s11:copy()を使ったコピー
s12:「=」を使ったコピー


それぞれのメモリ情報は

以下のようになります。


一行目のアドレスは、

スライス自体のアドレスですので、

もちろん全て異なります。



2行目はスライスが示す

配列の情報です。



データは配列のアドレスを示してします。

s8 と s12 が同じアドレスだと分かります。

つまり「=」でコピーすると、

参照先アドレスが同じになります。



これが意味するのは、

コピー元の配列の値が変われば、

コピー先であるs12の値も変わります。

もちろん逆もしかりです。




s8[0] = 3として値を変えてみます。

s11は変わりませんが、

s12は変わってしまいます。



これがcopy()と「=」の違いです。



もう少しコピーについて

サクッとみていきましょう。




コピー先の要素数が足らなくて、

要素数が異なっていても

コピーはしっかり出来ます。


要素が足らないところは、

脱落するだけです。




コピー元のデータを一部だけ

コピーしたい場合についてみてみましょう。


[数字:数字]のようにすることで、

任意な要素列を取り出すことが出来ます。




先は、コピー元の要素を指定しました。

逆に、コピー先の要素を指定すると

どうなるのでしょうか。



コピー先を指定した場合は、

任意の位置からコピーできます。




続いて要素の追加をしていきましょう。

組み込みのappend()関数を使います。

メモリの話も絡めてお話します。

またちょっとした応用もご紹介しています。



append()関数は最後の要素に

新しく要素を追加する関数です。

 



構文は、append(スライス, 数字, ...)です。

一度に複数の値も追加出来ます。




出力結果はこのようになります。





これを見てみると、

make()で初期化をしていないのに、

append()したらスライスの参照先の

配列のアドレスが変わっています。



もちろん長さが変わるのと、

容量であるcapも変わります。




続いてappend()の応用を

少しご紹介します。



copy()のところで説明した

[数字:数字]を使うことで、

様々な操作に応用できます。



上のソースで紹介しているのは、

要素のカットです。



s17 = append(s17[1:], 4)

このように書くことで、

要素番号1以上を抜き取って、

その最後に4を追加する

という意味になります。



したがって出力結果は、

[2 3 4] になります。


他にも応用に仕方がありますが、

長くなってしまいますので、

またの機会に紹介したいと思います。




それでは最後に、

「スライスと構造体」「スライスと配列」

について説明します。



スライスを含む構造体



まずは例えば以下のように、

構造体を宣言します。


type SliceStruct struct {
  Integer int
  Slice []int
}



代入のやり方には、

例えばこのような書き方があります。



構造体でもスライスへの代入方法は

大体同じ感じですが、

初期化してからでないと

スライスメンバへの代入は出来ないです。


配列をスライスに結び付ける



配列とスライスは、

親密な関係にあります。



配列として宣言したものは、

後にスライスに渡すことが出来ます。



こんな感じです。

スライス名と配列の要素を

そのまま等式で結ぶだけです。




以上です。

最後まで読んでいただきありがとうございました。



-プログラミング

Copyright© MasaのITC Life , 2023 All Rights Reserved Powered by STINGER.