こんにちは。
Women Who Go Tokyo の micchie と申します。
Go4 Advent Calendar 2019 の22日目の記事を担当します。
Women Who Go Tokyo は、毎月1回 Go についての勉強会を行っています。
Go にはじめて触れる方にも安心して楽しく参加していただけるよう、少しずつ改良を重ねながら運営しています。
最近は「Go のことを学びながらやっていたら、いつのまにか家計簿アプリができていた!すごい!」を体験していただけるように「家計簿アプリを作ろう!」というタイトルやっています。
教材を提供してくださっている @tenntenn さん、いつもありがとうございます。
今日のアドベントカレンダーでは、質問をもらうことが多い箇所について紹介したいと思います。
今後の勉強会の助けになりますように。
説明が怪しいところは是非、正していただけますと幸いです。
※ ちなみに、すでに大変良い形でまとめてくださっている素敵なブログが、たくさんあります!
1. ポインタがわからない
ポインタとは、データ本体のアドレスを指すものです。
あまり良い例ではありませんが「上野動物園のクマ舎にいるマレーグマ」を想像してみてください。
- データ本体: マレーグマ
- アドレス: 上野動物園のクマ舎
bear という変数に “マレーグマ” を格納します。
var bear string
bear = "マレーグマ"
bear は “マレーグマ” の本体です。
fmt.Printf("%s\n", bear)
fmt.Printf("%p\n", &bear)
上記を実行すると、データ本体とそのアドレスを出力できます。
bearのデータ本体: マレーグマ
bearのアドレス: 0x40c138
0x40c138
が「上野動物園のクマ舎」を表す「アドレス」です。
&
をつかってデータ本体からそのアドレスを取り出しています。
次に、b に bear を代入します。
b := bear
代入は値のコピーです。
fmt.Printf("bのデータ本体: %s\n", b)
fmt.Printf("bのアドレス: %p\n", &b)
上記を実行すると、
bのデータ本体: マレーグマ
bのアドレス: 0x40c150
このようになります。
データ本体は同じに見えますが、アドレスが bear と b は異なります。
マレーグマが 0x40c150
という「上野動物園のクマ舎 (2)」にコピーされたということになります。(しかしクマがコピーされるなんて…物理的にはありえない…)
今度は p というポインタ変数を作って、bear のポインタを代入します。
var p *string
p = &bear
*string
は string のポインタ型です。
fmt.Printf("pのデータ本体: %s\n", *p)
fmt.Printf("pのアドレス: %p\n", p)
上記を実行すると、
pのデータ本体: マレーグマ
pのアドレス: 0x40c138
このようになります。
ポインタ変数に *
を付与することで、そのデータ本体を取り出すことができます。
また、bear のアドレスを代入しているので、同じアドレスとなります。
2. 配列とスライスがむずかしい
Go には配列とスライスがあります。この時点で「スライスとは?」みたいな気持ちになる人も少なくはないようです。
配列を作る
配列は、同じ型のデータを集めたもの、スライスは配列の一部となります。
まずは、長さが 3 の string 型の配列を作成します。
array := [3]string{"シロクマ", "ツキノワグマ", "アメリカクロクマ"}
配列は、長さも含めて一つの型となります。
そのため、要素を追加したい場合は、その長さにあった配列を作り直さなければなりません。
array[0] = "メガネグマ"
array[1] = "メガネグマ"
array[2] = "メガネグマ"
このように値の書き換えはできますが、長さを超えると、
array[3] = "メガネグマ"
エラーとなります。
invalid array index 3 (out of bounds for 3-element array)
スライスを作る
スライスを作ります。
slice := []string{"シロクマ", "ツキノワグマ", "アメリカクロクマ"}
一見、上の配列と同じように見えますが、実態は下記のようになっています。
1. 長さが 3 の string 型の配列が作られる。
2. 1. を参照したスライスが作られる。
このようにスライスを作ることもできます。
この make
の段階では、各要素はスライスの型のゼロ値 (この場合は string なので空文字) が格納されます。
slice := make([]string, 3)
slice[0] = "シロクマ"
slice[1] = "ツキノワグマ"
slice[2] = "アメリカクロクマ"
スライスから取り出す
スライスの値は範囲を指定して取り出すことができます。
// slice全体: [シロクマ ツキノワグマ アメリカクロクマ]
fmt.Printf("slice全体: %v\n", slice)
// 最初〜最後まで: [シロクマ ツキノワグマ アメリカクロクマ]
fmt.Printf("最初〜最後まで: %v\n", slice[:])
// インデックス0〜最後まで: [シロクマ ツキノワグマ アメリカクロクマ]
fmt.Printf("インデックス0〜最後まで: %v\n", slice[0:])
// インデックス1〜最後まで: [ツキノワグマ アメリカクロクマ]
fmt.Printf("インデックス1〜最後まで: %v\n", slice[1:])
// インデックス2〜最後まで: [アメリカクロクマ]
fmt.Printf("インデックス2〜最後まで: %v\n", slice[2:])
// 最初〜インデックス1-1=0まで: [シロクマ]
fmt.Printf("最初〜インデックス1-1=0まで: %v\n", slice[:1])
// インデックス1〜インデックス1-1=0まで: []
fmt.Printf("インデックス1〜インデックス1-1=0まで: %v\n", slice[1:1])
// インデックス1〜インデックス2-1=1まで: [ツキノワグマ]
fmt.Printf("インデックス1〜インデックス2-1=1まで: %v\n", slice[1:2])
// インデックス1〜インデックス3-1=2まで: [ツキノワグマ アメリカクロクマ]
fmt.Printf("インデックス1〜インデックス3-1=2まで: %v\n", slice[1:3])
// panic: runtime error: slice bounds out of range [5:3]
fmt.Printf("インデックス5〜最後まで: %v\n", slice[5:])
スライスに追加する
スライスは値の追加ができますが、長さ以上の要素を直接入れようとすると、エラーになります。
slice[3] = "メガネグマ"
.
.
.
panic: runtime error: index out of range [3] with length 3
しかしスライスは、配列と違って長さを持たないため、append
を利用して要素の追加ができます。
slice = append(slice, "メガネグマ")
スライスの cap と len
スライスには cap
という容量を表すものと、len
という長さを表すものがあります。
下記は、cap と len を指定しながらスライスを作った例です。
slice := make([]string, 3, 3)
fmt.Println(slice, len(slice), cap(slice))
1. 長さが 3 の string 型の配列が作られる。
2. 1. を参照したスライスが作られる。
このときの 1. のサイズが cap
、配列に入っている要素の数が len
です。
要素は string のゼロ値が入っています。
[ ] 3 3
ここで、要素をいくつか足します。
slice = append(slice, "アメリカクロクマ")
fmt.Println(slice, len(slice), cap(slice))
slice = append(slice, "ツキノワグマ")
fmt.Println(slice, len(slice), cap(slice))
slice = append(slice, "ホッキョクグマ")
fmt.Println(slice, len(slice), cap(slice))
slice = append(slice, "マレーグマ")
fmt.Println(slice, len(slice), cap(slice))
最初に3つ、string のゼロ値である空文字が格納されたあとに 1 つ追加したため、len
は 4 です。
[ アメリカクロクマ] 4 6
[ アメリカクロクマ ツキノワグマ] 5 6
[ アメリカクロクマ ツキノワグマ ホッキョクグマ] 6 6
[ アメリカクロクマ ツキノワグマ ホッキョクグマ マレーグマ] 7 12
cap
に注目してください。
append
を行ったときに、cap
よりも大きくなってしまう場合 cap
のメモリを拡張するのですが、拡張前の cap
の倍を確保しに行こうとしています。
- アメリカクロクマを追加する前は、cap が 3 で、 6 に拡張。
- マレーグマを追加する前は、cap が 6 で、12 に拡張。
ある程度の値が増えることがわかっている場合は、最初からスライスの cap
を指定することで、メモリの効率を考慮した書き方ができます。
3. Range が期待をうらぎる
Go ではスライスのループを利用することがよくあります。
たとえば、下記のようなスライスを作成して、順番にクマの名前とアドレスを表示するプログラムを書きます。
bears := []string{"シロクマ", "ツキノワグマ", "アメリカクロクマ"}
for _, bear := range bears {
fmt.Printf("range bearのデータ本体: %s\n", bear)
fmt.Printf("range bearのアドレス: %p\n\n", &bear)
}
上記を実行すると、
range bearのデータ本体: シロクマ
range bearのアドレス: 0x40c138
range bearのデータ本体: ツキノワグマ
range bearのアドレス: 0x40c138
range bearのデータ本体: アメリカクロクマ
range bearのアドレス: 0x40c138
このようになります。何か気づきませんか…?
同じアドレスです。値のコピーは作成されないのです。
これに気づかない状態で、サクッと並列化します。
for _, bear := range bears {
go func() {
fmt.Printf("bearのデータ本体: %s\n", bear)
fmt.Printf("bearのアドレス: %p\n\n", &bear)
}()
}
time.Sleep(1 * time.Second)
上記を実行すると、
./prog.go:12:47: loop variable bear captured by func literal
./prog.go:13:47: loop variable bear captured by func literal
Go vet exited.
bearのデータ本体: アメリカクロクマ
bearのアドレス: 0x40c138
bearのデータ本体: アメリカクロクマ
bearのアドレス: 0x40c138
bearのデータ本体: アメリカクロクマ
bearのアドレス: 0x40c138
期待した結果になりますでしょうか。期待から外れています。
ループ変数である bear は各ループで変更されるため、goroutine は bears の最後の値を利用します。
これを回避するには、下記のようにパラメータで渡したり、値をコピーすると良いでしょう。
for _, bear := range bears {
go func(bear string) {
fmt.Printf("bearのデータ本体: %s\n", bear)
fmt.Printf("bearのアドレス: %p\n\n", &bear)
}(bear)
}
for _, bear := range bears {
bear := bear
go func() {
fmt.Printf("bearのデータ本体: %s\n", bear)
fmt.Printf("bearのアドレス: %p\n\n", &bear)
}()
}
最後に
毎月開催されている Women Who Go Tokyo ですが、色んな人に Go を楽しんでもらいたいと思っています。
こちらの記事 Women Who Go Tokyo という取り組みでわたしたちは。|micchie|note でも紹介しているので、興味のある方はぜひ、覗いてみてください。
※ 本当はレシーバの話も書きたかった…間に合わなかった…