Small Changes

たった1日で基本が身に付く! Go言語 超入門@4日目


Metadata


もう4日目に突入してしまいました。

1章1時間計算で8章あるので8時間。

1日でいけるかなーと思っていましたが、一気には無理でしたね。

今日も少しずつ進めていこうと思います。

CHAPTER 6 データを直接指し示すポインタ

変数に変数を代入する

ポインタの前に、以下の場合を見ていく

a := 5
b := a
a = 7
fmt.Println(a) // 7
fmt.Println(b) // 5

bにaを代入したあとに、aに7を代入している。

でもbは5のままで変わっていないことがわかる。

メモリの位置(アドレス)を呼び出す

次にメモリの位置を見ていく。

変数の前に「&」をつけると、メモリの場所を呼び出せる

Printf関数でポインタを表す「%p」を使う

a := 5
b := a
a = 7
fmt.Println(a) // 7
fmt.Println(b) // 5
fmt.Printf("aのメモリの位置:%p\n", &a) // aのメモリの位置:0xc0000140a0
fmt.Printf("bのメモリの位置:%p\n", &b) // bのメモリの位置:0xc0000140a8

aとbでは値の保存場所が違うことが確認できる。

変数に変数を代入しても別の場所に保存されるため、安心して使うことができる。

アドレスから値を呼び出す

アドレスから値を呼び出すには、アドレスの前に「*」を付けます。

a := 5
b := &a
fmt.Printf("bの値:%p\n", b) // bの値:0xc0000140a0
fmt.Printf("aのアドレス:%p\n", &a) // aのアドレス:0xc0000140a0
// aのアドレスとbの値は同じ
// ではbから値を呼び出すには?
fmt.Printf("bの値:%d\n", *b) // 5

*[アドレスの入った変数]でポインタ表現

アドレスを使って書き換える

変数a

変数bは、aのアドレス

*b = 7でaのアドレスの値を変更する。

変数aと変数bは同じアドレスをみているので、どちらも同じ7になる。

a := 5
b := &a
b = 7
fmt.Printf("aの値:%d\n", a)  // 7
fmt.Printf("bの値:%d\n", *b) // 7

異なる変数から同じ値を操作できるという点で有用

「変数がどのアドレスを見ているか」と「どのポインタ表現(*hoge)に対して値を代入したか」に注目すると個人的には混乱しにくかった。

ちょっと勘違いしていたが下記で良い気づきが得られた。あくまでもアドレスを代入する変数は値としてアドレスを持つだけで、変数自身のアドレスは別である。

inta := 5
adinta := &inta
bdinta := &inta
bdinta = 9
fmt.Printf("inta のアドレスは %p\n", &inta)     // inta のアドレスは 0xc0000140a0
fmt.Printf("adinta のアドレスは %p\n", &adinta) // adinta のアドレスは 0xc000006028
fmt.Printf("bdinta のアドレスは %p\n", &bdinta  // bdinta のアドレスは 0xc000006030

クロージャの謎

4章で出てきたクロージャの謎もなんで同じctr変数を使いまわし続けられるのかがやっとわかった!

ちゃんとしたことまでは分かっていないけど、一回目にcounter実行後の返り値である無名関数の中でctrは有効のままなので、再度呼び出すときにもメモリの同じ位置を書き換えてくれる。

でも、クロージャでなくてもポインタでも同じことできそう。

package main
import "fmt"
func counter() func() {
    - ctr := 0 // ① 変数ctrを宣言。メモリ上に場所を確保
    - fmt.Println("カウンタを初期化しました")
    - fmt.Printf("カウンタのアドレスは%p\n", &ctr)
    - return func() {
        - ctr++ // ② ①で確保したメモリの場所に値を保存
        - fmt.Printf("カウンタの値は%d、", ctr)
        - fmt.Printf("カウンタのアドレスは%p\n", &ctr)
        - }
}
func main() {
    - countfnc := counter() // ③ counterを実行すると①、②が実行される。戻り値の無名関数がcountfncに代入。
    - countfnc() // ④countfnc内では①で宣言したctrが有効なのでカウントアップできる。以降同じ
    - countfnc()
    - countfnc()
}

引数にポインタ表現を取るcounter関数

引数adctrのデータ型は「*int」 => adctrはint参照型 *adctrとしてアドレスを参照したらint型だから

  • ポインタの使用をデータ型として表す記法

  • 「*adctrのデータ型はint」という意味

  • C言語などポインタを用いる言語では共通に参照型と呼ばれている

クロージャと違ってシンプル

ポインタを使えばそのアドレスの数値を書き換えて作業が終了する

package main
import "fmt"
func counter(adctr *int) {
    - adctr++ // ctrのアドレスの値をインクリメント。戻り値は不要
}
func main() {
    - ctr := 10 // ctrを宣言
    - counter(&ctr) // ctrのアドレスを渡す
    - fmt.Println(ctr)
}

ここまでの応用

サイコロを使ったプログラム

全体像

package main
import "fmt"
type roll struct {
    - round int
    - score int
}
func begin() *roll {
    - fmt.Println("サイコロを投げます")
    - return &roll{0, 0} // ① 構造体rollのインスタンスのアドレスを返す
}
func throw(r *roll, x int) {
    - r.round++ // ③ 本来は*rと書かないといけない
    - if x%2 == 0 {
        - r.score++
    - }
    - fmt.Printf("%d回目: スコア=%d\n",
        - r.round, r.score)
}
func main() {
    - myroll := begin()
    - throw(myroll, 6) // ② &が不要
    - throw(myroll, 5)
    - throw(myroll, 2)
    - throw(myroll, 4)
    - throw(myroll, 3)
}

①の解説

ここでは、構造体rollのインスタンスのアドレスを返している。

戻り値は「*roll型」とする

  • *をつければroll型になるという意味

func begin() *roll {
    - fmt.Println("サイコロを投げます")
    - return &roll{0, 0} // ① 構造体rollのインスタンスのアドレスを返す
}

②の解説

throw関数は引数に構造体のrollのアドレスを受け取る関数になっている。

①でrollのアドレスが返ってくるのでここでは&が不要

throw(myroll, 6) // ② &が不要

③の解説

rはrollのアドレスなので、実際にrollインスタンスとして扱うには*rとする必要がある。

だが、関数の中では「*r」を省略して「r」として書いて良いのでrとかける!!

func throw(r *roll, x int) {
    - r.round++ // ③ 本来は*rと書かないといけない
    - if x%2 == 0 {
        - r.score++
    - }
    - fmt.Printf("%d回目: スコア=%d\n",
        - r.round, r.score)
}

今日の学び

6章でクロージャの謎がわかるという胸熱展開。

構造体のインスタンスにもポインタ表現を使って、アドレスから内容を読み書きできる。

同じ変数や構造体を使いまわしたいなら、そのアドレスを渡してアドレスの値に対して読み書きすれば間違いない

6章走り切りたかったけど、集中力がもたなかったのでここまで。

References