17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go4Advent Calendar 2019

Day 22

Go を一緒に学んでいるときに質問されやすいところ3セット

Last updated at Posted at 2019-12-22

こんにちは。
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 のアドレスを代入しているので、同じアドレスとなります。

The Go Playground

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)
	}()
}

The Go Playground

最後に

毎月開催されている Women Who Go Tokyo ですが、色んな人に Go を楽しんでもらいたいと思っています。
こちらの記事 Women Who Go Tokyo という取り組みでわたしたちは。|micchie|note でも紹介しているので、興味のある方はぜひ、覗いてみてください。

※ 本当はレシーバの話も書きたかった…間に合わなかった…

17
12
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
17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?