0
0

More than 3 years have passed since last update.

参照型について

Last updated at Posted at 2020-11-05

前提

参照型とは

それ自体は含まない、そのデータの場所を指し、保持するものである。といった具合に、言葉では表現し難いデータ構造です。

Goにはそれぞれ、スライスマップチャネルという3つの参照型が定義されています。

組み込み関数makeとは

スライス・マップ・チャネルの3つの参照型を生成できる、組み込み関数です。型によってパターンが違ったりするので、各理解が必要になります。

lenとcap

この2つの組み込み関数は、3つの参照型に対して有効に使用できるので、前提知識として押さえておきます。

len 各型の要素数を調べることができます。
cap 各型の容量(キャパシティ)を調べることができます。

スライス

スライスとは

可変長配列のようなものを表現するデータ構造です。

前述の通り、組み込み関数makeで型を生成することができます。

a := make([]int, 5)
a[0] = 1
fmt.Println(a)  //[1 0 0 0 0]

容量が5であるint型のスライスを生成し、任意のインデックス値を指定したものを定義しています。
生成方法や挙動が配列型と非常に似ていますが、配列型と違い、関数間の呼び出しなどのやり取りの際に、一方での処理の影響を受けるという点があります。

スライスのリテラル

スライスには、makeを使わずに生成するためのリテラルが用意されています。

a := []int{ 0, 1, 2, 3, 4 }
fmt.Println(a)  //[0 1 2 3 4]

簡易スライス式と完全スライス式

簡易スライス式完全スライス式という、既存のスライスをもとにパラメータを取り、新たにスライスを生成する機能があります。

まず、簡易スライス式はスライスのインデックスに関する範囲のパラメータをとり、そのパラメータが表すスライスの一部を抜き出して、新しいスライスを作成します。

a := []int{ 0, 1, 2, 3, 4 }
fmt.Println(a[0:4])  //[0 1 2 3]

記述方法については、取るパラメータやなどによってバリエーションがあるので、理解が必要です。

次に、完全スライス型は簡易スライス型と違い、low : high : maxの3つのパラメータをとることができます。

a := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }

b := a[2:6:8]
fmt.Println(len(b))  //4
fmt.Println(cap(b))  //6

通常の配列型やスライスは、要素数 - lowの値というようにして容量が決まっていましたが、完全スライス型の形でパラメータを取ると、max - lowの値で容量が決まるといった違いがあります。

また、どちらのスライス式でも文字列を参照することができます。しかし、ASCIIで表現しきれなかった場合のマルチバイト文字については、[ ]byteを単位とするため、バイト列におけるインデックス範囲を指定する必要があります。

appendとcopy

組み込み関数であるappendcopyはどのような機能を持っているのかをみていきます。
append スライスの要素の追加ができる機能です。
copy スライスの要素をコピーできる機能です。

append.go
a := make([]int, 0, 0)
fmt.Println(cap(a))
a = append(a, []int{ 1, 2, 3 }...)
fmt.Println(a)  //[1, 2, 3]
fmt.Println(cap(a))  //4
a = append(a, []int{ 4, 5, 6 }...)
fmt.Println(a)  //[1, 2, 3, 4, 5, 6]
fmt.Println(cap(a))  //8

要素数0で容量0のスライスの末尾に、要素を追加し、容量の変動を確認しています。このプログラムから、スライスでは容量をオーバーして要素を追加した際に、自動拡張される性質があります。
一見倍増しているように見えますが、闘値やランタイムに依存しているため、一概に倍増しているわけではないことに注意が必要です。

上記の場合では、appendの戻り値としてメタデータを取っているため、appendが反映されてcapで値を出力できているのですが、メタデータへ間接的にappendを行っても、反映されないという性質を持っています。

append.go
func test(a []int) {
   a = append(a, 3)
}

func main() {
   b := make([]int, 1, 2)
   test(b)
   fmt.Println(cap(b))  //2
}
copy.go
a := []int{ 1, 2, 3, 4, 5 }
b := []int{ 6, 7, 8 }
fmt.Println(copy(a, b), a)  //3 [6 7 8 4 5]

コピーした要素数と、コピーされた要素が出力されています。このように、コピー対象の先頭から要素が上書きコピーされています。もし、コピーする要素数がコピー対象の要素数を超えていた場合でも、コピー対象の要素数分だけコピーされます。

また、文字列型もコピーすることはできますが、スライス式のところで前述したのと同じに、バイト単位でコピー処理されることに考慮する必要があります。

マップ

マップとは

連想配列のようなものを表現するデータ構造です。

マップも例に倣って、組み込み関数makeで生成することができます。

a := make(map[int]string)

a[1] = "test1"
a[2] = "test2"
fmt.Println(a)  //map[1:test1 2:test2]

int型のキー値と、string型の要素の型を定義し、型に沿ったキー値と要素を代入して出力しています。
また、要素数に対して、2つ目の引数にメモリ領域を確保するヒントを取ることで、マップの規模が多き場合には有用に使用することができます。

マップのリテラル

マップにもmakeを使わずに生成するためのリテラルが用意されています。

a := map[int]string{ 1: "test1", 2: "test2" }

fmt.Println(a)

要素は、可読性を上げるために複数行に分けて書くこともできますが、Goの特性上、最後の値の末尾には継続を意味するカンマを記述する必要があります。

マップ内でスライスを生成することもできます。

a := map[int][]int{
   1: []int{1}
   2: []int{2}
}
fmt.Println(a)  //map[1:[1] 2:[2]]

ちなみに、要素の指定での[ ]intは省略することが可能なので、[ ]intと書くメリットは特にないのではないかと思います。

要素の参照方法

変数にa[1]のように代入することで、それに対応するキー値の要素を参照することができるのですが、Goでは基本型はnilの状態を持たないため、型の初期値を要素におくと保持したまま処理されてしまいます。しかし、2つの変数に代入するようなイディオムを使用することで対策することができます。

a := map[int]string{ 1: "test1", 2: "test2" }

if a, ok := a[1]; ok {
   fmt.Printf("%v, %v\n", a, ok)  //test1, true
} else {
   fmt.Printf("%v, %v\n", a, ok)  //※  , false

上記のプログラムでは、2つ目の変数で、キーに対応する要素があるか評価し、bool型で代入されます。それを条件式に沿って分岐処理を行い、出力しています。もちろん、代入したキー値がマップ内に存在していない場合は、elseが実行され、空の文字列とfalseが出力されます。

delete

マップから要素を削除するための組み込み関数です。

a := map[int]string{ 1: "test1", 2: "test2" }

fmt.Println(len(a))  //2

delete(a, 2)
fmt.Println(len(a))  //1

delete前と後での要素数の変化をlenを使って比較しています。delete前では、マップ内の要素数通りの結果が出力されているのに対し、deleteを使用したことで、要素数が減っていることが分かります。deleteは、任意のキー値を指定することで削除できるので、上記のプログラムではキー値が2である"test2"が削除されているということになります。

ちなみに、マップでは実質的には容量は存在するものの、性質上capを使用することはできません。

チャネル

チャネルとは

前回の記事(制御構文について)でまとめた、ゴルーチン間でのデータの送受信を担っているデータ構造です。

チャネルには、チャネルの型というものが存在します。

var a chan int

後に見ていきますが、受信専用チャネル送信専用チャネルといったサブタイプを指定することができます。指定しない場合には、送受信どちらともが可能になります。

makeで生成すると下記のようになります。

a := make(chan int, 5)

バッファサイズという、一時的にデータを保持しておける領域の容量を指定しておくことができます。

受信と送信

チャネルを使用してデータの送受信をします。

a := make(chan int, 5)

a <- 1  //送信
a <- 2  //送信
fmt.Println(cap(a))  //5
fmt.Println(len(a))  //2

b := <-a  //受信
fmt.Println(b)  //  1

fmt.Println(cap(a))  //5
fmt.Println(len(a))  //1

上記のプログラムでは、バッファサイズ5のチャネルを生成し、チャネルの送受信をしたときのチャネル内のデータの比較を行っています。
まず、チャネルaに1と2の値を送信した時の容量と要素数を見てみると、容量はバッファサイズにあたるので、値は5になり、2つの値を送信したので要素数は2になります。そこで、値を1つ変数bで受信します。そうすると、キャパシティは変わることはありませんが、要素数は1つ減りました。また、変数bから1が出力されていることから、先入先出しの関係でデータを送受信していることがわかります。

ゴルーチン

では、ゴルーチン間でのデータの送受信を行ってみます。

func test(a <- chan int) {
   for {
      b, ok := <-a
      if ok == false {
         break
      }
      fmt.Println(b)  //1
   }                  //2
}                     //3
                      //4

func main() {
   a := make(chan int)
   go test(a)

   b := 1
   for b < 5 {
      a <- b
      b++
   }
   close(a)
}

上記のプログラムでは、任意の引数を取ったチャネルの受信用の関数testを定義し、チャネルaがクローズ(falseの値を返す)されている場合に、出力中のループを中断する分岐処理を行っています。そして、関数mainでは、関数testで使用しているチャネルaを生成して、値が5未満になるまで増分する変数bを送信し続けているゴルーチンを共有し、関数testはチャネルから受信した値を出力し続けています。
データの送信が完了するタイミングでクローズしているため、値が5以上になると、2つ目の引数であるokがfalseを返し、処理が終わるということです。

select文

変数への受信を表すチャネルが2つ以上ある場合、前のチャネルが受信できないとゴルーチンは停止してしまいます。制御構文であるselectは、この問題を解消できます。

a := make(chan int, 1)
b := make(chan int, 1)
b <- 5

select {
case <- a:
   fmt.Println("x is done")
case <- b:
   fmt.Println("y is done")  //出力
default:
   fmt.Println("?")
}

変数aはチャネルが生成されていますが、送信処理がされていないため受信できません。通常ならこの時点でゴルーチンは停止してしまうのですが、select構文を使用しているため、処理が続行しています。
また、もし複数caseの処理が成功した時は、ランダムで実行されます。

最後に

今回は参照型についてまとめました!!参照型は内容が濃くて、Goプログラムでも頻出するイディオムばかりなので、しっかり理解したいところです。

参考文献

著者 松尾愛賀
翔泳社 スターティングGo言語

0
0
4

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
0
0