#はじめに
こちらの続きです。
Go初学者が学んだことまとめ〜その1〜(構成と実行、構文)
今回は参照型についてです。
#参照型
##スライス
###定義
可変長配列を表現する型。
下記のように定義できる。
var s []int
s := make([]int, 5) //[0, 0, 0, 0, 0]
s := []int{1, 2, 3} //[1, 2, 3]
###要素数と容量
スライスには要素数と容量がある。
要素数はlen,容量はcapで取得できる。
//makeの第2引数が要素数、第3引数が容量
s := make([]int, 5, 10)
fmt.Println(len(s)) //5
fmt.Println(cap(s)) //10
容量とは、メモリ上に確保する領域のこと。
要素数を拡張していく際に、新たな領域の確保が不要になるというメリットがある。
###append
配列の拡張ができる。
s := []int{1, 2, 3}
s = append(s, 4) // s == [1, 2, 3, 4]
s = append(s, 5, 6, 7) // s == [1, 2, 3, 4, 5, 6, 7]
拡張される容量は、要素数と同じではない。
ランタイムが自動で拡張している。
どの程度拡張されるかは、Goのランタイムに依存する。
s := []int{1, 2, 3}
fmt.Println(cap(s)) //3
s = append(s, 4)
fmt.Println(cap(s)) //6
s = append(s, 5, 6, 7)
fmt.Println(cap(s)) //12
appendした時は、自動拡張するかどうかわからないということになる。
つまりこういうこと。
s1 := append(s0, x) //s0とs1が同じメモリ領域であるかは不明
なので、自動拡張の有無を意識しないといけないコードは基本NG。
###簡易スライス式
配列やスライスから、新たなスライスを生成する。
インデックスの範囲を[m:n]の形式で指定する。
a := [5]int{1, 2, 3 ,4, 5}
s := a[0:2] //s == [1, 2] sは[]int型
###完全スライス式
3つのパラメータを取る記法。
a[low:high:max] //0 <= low <= high <= max <= cap(a) という関係を満たす必要がある
maxという値で容量をコントロールできる。
a := [10]int{1, 2, 3 ,4, 5, 6, 7, 8, 9, 10}
s1 := a[2:4]
fmt.Println(cap(s1)) // 8 //len(a)-low が容量になる
s2 := a[2:4:4]
fmt.Println(cap(s2)) // 2 //max-low が容量になる
s3 := a[2:4:6]
fmt.Println(cap(s3)) // 4 //max-low が容量になる
「容量」という概念が非常にややこしいので注意。
###配列とスライスの引数
スライスは「参照型」であるので、スライスの配列要素の参照(ポインタ)を渡すことになる(詳細は後述)。
それに対して配列は、関数側に配列のコピーされたものが渡される。
下記は動作の違いを示したもの。
func powArray(a [3]int) {
for i, v := range a {
a[i] = v * v
}
}
func powSlice(s []int) {
for i, v := range s {
s[i] = v * v
}
}
func main() {
a := [3]int{1, 2, 3}
powArray(a) //コピーされたものが関数に渡るのでaは変更されない。
fmt.Println(a) //[1, 2, 3]
s := []int{1, 2, 3}
powSlice(s) //関数はsの配列要素と同一のメモリ領域を使用するためsは変更される。
fmt.Println(s) //[1, 4, 9]
}
ちなみに、JavaScript(TypeScript)の配列は参照渡し。
プリミティブ型(number,string,boolean)は値渡し、それ以外は参照渡しになる。
function pow(a: number[]): void {
for(let i = 0; i < a.length; i++) {
a[i] = a[i] * a[i];
}
}
const ary = [1, 2, 3];
pow(ary);
console.log(ary); //[1, 4, 9];
####さらに深堀すると・・・
スライスの内部構造は
- 配列へのポインタ
- 要素数 (length)
- 容量 (capacity)
の3つからなる。
関数の引数にスライスを渡した場合、上記の3つが値渡しされている。
下記はそれを示す例。
func add(s []int) {
s = append(s, 1) // s => ptr = 0xc0000180a0, len = 1, cap = 10
fmt.Printf("%p\n", s) // 0xc0000180a0 配列のアドレスは呼び出し元と同じ
fmt.Printf("%p\n", &s) // 0xc00000a0a0 スライス自体のアドレスは呼び出し元と違う
fmt.Println(s) //[1]
}
func main() {
s := make([]int, 0, 10) // s => ptr = 0xc0000180a0, len = 0, cap = 10
fmt.Printf("%p\n", s) // 0xc0000180a0 配列のアドレス
fmt.Printf("%p\n", &s) // 0xc00000a060 スライス自体のアドレス
add(s)
fmt.Println(s) // [] len = 0なので出力は[]になる
}
appendすると参照元のスライスは書き換わるように見えるが、実際の出力は書き換わっていないように見える。
これは、スライスの要素数は値渡しのため、引き渡した関数側でappendして要素数を書き換えても、参照元には反映されないためである。
##マップ
###定義
連想配列を表現する型。
下記のように定義できる。
var m map[int]string
m := make(map[int]string)
m := map[int]string{1: "Taro", 2: "Hanako", 3: "Jiro"}
特別な使い方はないので、省略。
##チャネル
ゴルーチンとゴルーチンの間でデータを受け渡しするためにデザインされたGo特有のデータ構造。
###定義
var ch chan int //双方向
var ch1 <-chan int //受信専用
var ch2 chan<- int //送信専用
ch := make(chan int, 8) //バッファサイズ8
チャネルはFIFO(先入先出し)のキュー。
バッファサイズとはこのキューのサイズ。
###使い方
チャネルは必ずゴルーチンと一緒に使う。
下記は0〜10000をゴルーチン間で受け渡し、出力するプログラム。
package main
import "fmt"
func receiver(ch <-chan int) {
for {
i, ok := <-ch
if ok == false {
//受信できなくなったら終了
break
}
fmt.Println(i)
}
}
func main() {
ch := make(chan int)
go receiver(ch)
i := 0
for i <= 10000 {
ch <- i
i++
}
//closeするとi, ok := <-chのokがfalseになる
close(ch)
}
###select
複数のチャネルをコントロールする構文。
例えば
e1 := <- ch
e2 := <- ch
というコードで、変数ch1のチャネルからデータが受信できない場合、このゴルーチンは停止したままになり、ch2はいつまでたっても受信できない。
このように、1つの処理で複数のチャネルを処理できない問題に対処できるのがselect。
ゴルーチンを停止させることなく、全ての処理を継続させることができる。
このように書く。
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
go func() {
for {
i := <- ch1
ch2 <- (i * 2)
}
}()
go func() {
for {
i := <- ch2
ch3 <- (i - 1)
}
}()
n := 1
LOOP:
for {
//処理が継続できるcaseの中からランダムに選択される
//全て処理が継続できない場合はdefaultが選択される
select {
case ch1 <- n:
n++
case i := <- ch3:
fmt.Println("received", i)
default:
if n > 100 {
break LOOP
}
}
}
}
非同期処理のデータ送受信がここまで簡単にできるのがGoの特徴。
##Goは全て値渡し
When are function parameters passed by value?
everything in Go is passed by value
公式がおっしゃる通り、Goは全て値渡しとのことです。
Goで参照渡しという言葉を使うのは誤解を招くのでやめましょう。
(この記事も当初は参照渡しという言葉を使っていましたが、ご指摘により変更しました。)
#まとめ
参照型についてまとめました。
スライスや、チャネルを使ったselectなどは少し難易度が上がりました。
selectをきちんと理解できれば、Go初心者は卒業できるのかなと思います。
その3に続きます。
#参考
スターティングGo言語
#関連
Go初学者が学んだことまとめ〜その1〜(構成と実行、構文)
Go初学者が学んだことまとめ〜その3〜(構造体、インターフェース)
Go初学者が学んだことまとめ〜その4〜(コマンド、パッケージ)