概要
この3日間ほど、A Tour of Goに取り組んでいたのですが、想像以上に難しかったです。
自分が詰まったところについて、ちょっとだけ記録を残そうと思います。
Readers
この出力結果は次のようになっています。
n = 8 err = <nil> b = [72 101 108 108 111 44 32 82] b[:n] = "Hello, R" n = 6 err = <nil> b = [101 97 100 101 114 33 32 82] b[:n] = "eader!"
該当の出力するコードは次のようになっています。
fmt.Printf("n = %v err = %v b = %v\n", n, err, b) fmt.Printf("b[:n] = %q\n", b[:n])
この部分、最初はどういうことか分からなかったのですが、色々調べて分かりました。
重要なのは、[72 101 108 108 111 44 32 82]
と[101 97 100 101 114 33 32 82]
の比較です。
つまり、バイト数が8
のときは、全ての値が更新されています。
しかし、バイト数が6
のときは、最後の32
と82
は更新されていません(前から6つ分は更新されています)。
つまり、b[:n]
で、更新されたバイト数ぶんを受け取って、それを%q
で人が読める形に変換しているということです。
b
の初期値は[0 0 0 0 0 0 0 0]
になっています。for文の前に、
fmt.Println(r) fmt.Println(b)
このように埋め込むと分かりやすいと思います。
Readers その2
Exercise: Readers | A Tour of Go
まず結論から言うと、このコードの回答は次のようになります。
package main import "golang.org/x/tour/reader" type MyReader struct{} func (mr MyReader) Read(p []byte) (n int, e error) { for i := range p { p[i] = 'A'; } return len(p), nil } func main() { reader.Validate(MyReader{}) }
しかし、なぜこれが答えになるのかが今ひとつ理解できませんでした。
まず、reader.Validate
関数は次のようになります。
このコードの中で、重要な部分がここです。
b := make([]byte, 1024, 2048) i, o := 0, 0 for ; i < 1<<20 && o < 1<<20; i++ { // test 1mb n, err := r.Read(b) for i, v := range b[:n] { if v != 'A' { fmt.Fprintf(os.Stderr, "got byte %x at offset %v, want 'A'\n", v, o+i) return } }
まず、r
というのはfunc Validate(r io.Reader) {
から来ていて、これは先程作ったfunc (mr MyReader) Read(p []byte) (n int, e error)
を指しています。
そのため、r.Read(b)
で呼び出しています。
さて、b := make([]byte, 1024, 2048)
とは何でしょうか? これは、バイトスライスを作成しています。つまり、初期値の長さが1024
のスライスです。
[0 0 0 0 0 ... 0 0 0]
みたいな0
が1024入っていると考えると分かりやすいかもしれません。ちなみに、バイト型とは次のことを指します。
ここで「なぜ2048
なのか?」という疑問が湧きますが、それについてはあまり気にしなくて良さそうです。というのも、これはスライスの容量であり、おそらく何かしらの拡張性を担保するために付けられているのだと想像します。
そして、ここからが本題です。
n, err := r.Read(b) for i, v := range b[:n] { if v != 'A' { fmt.Fprintf(os.Stderr, "got byte %x at offset %v, want 'A'\n", v, o+i)
ここで自分は苦戦しました。
結論から言うと、r.Read(b)
が実行されることで、b
の値が書き換わっています。
(返り値がないので分かりにくいですが、ここでb
の値を変更していると考えると辻褄が合います)
つまり、こういうことです。
先程の[0 0 0 ... 0]
が、[65 65 65 ... 65]
に変わっているのです。
Hex to ASCII Text String Converter
A
に対応するHexadecimal(16進数)は41
です。そのため、%x
で65
が16進数に変換されて、41
になります(より正確には0x41
)。
先程のコードで、
for i := range p { p[i] = 'B'; }
と、'A'
ではなく'B'
を敷き詰めてあげると、良い感じにエラーを返してくれます。
got byte 42 at offset 0, want 'A'
42
を10進数にすると、66
、つまりASCIIにおける'B'
となります。
あとはRange
でループさせて、一つ一つ'A'
になっているか……つまり65
になっているかを確認している、ということになります。
ちなみに、for ; i < 1<<20 && o < 1<<20; i++
は、読み込みが1MBに達するまで、またはo
(総読み取りバイト数)が1MBに達するまで、ループを続けているようです。
1<<20
はビット演算を使っていて、つまりこのコードによって、00000000000000000001
が10000000000000000000
になります。これを十進数で表すと2 ** 20
であり、1,048,576
バイト……つまりは1メガバイトになります。
メガバイトとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
難しすぎるってばよ……!!
ImagesとExercise: Imagesについて
実はよく分かっていません……あとで復習します。
ポインタとアドレスについて
ポインタとアドレスって、結局どういうときに使うの?という疑問が湧いたので調べてみました。
自分が参考になったのはこの3つの記事です。
- 【Go】自分が理解に苦しんだ、ポインタとアドレスについてまとめる
- Go言語のポインタを解説 - Recursion
- Goで学ぶポインタとアドレス - Qiita
- [Golang] A Tour of GoのMethodsを理解する
特に1つ目の記事の、この部分が参考になりました。
ポインタはGoでいつ使うのか
Goでは、引数やレシーバを関数内で書き換える必要がある場合、書き換える対象はポインタで渡す必要があります。
2つ目の記事では、この部分が参考になりました。
これらの追加コストを回避するために、Goでは関数の引数や戻り値としてポインタを渡すことができます。ポインタが渡されると、関数はその値のコピーではなく、元の値の基礎となるメモリ位置にアクセスすることができます。つまり、関数内で値に変更を加えると、元の値にも影響が及びます。
3つ目の記事では、次の部分が参考になりました。
*namePoint
には、&name
(stringへのポインタである*string型の値が格納されているからであり、それを*namePoint = "二郎"
で書き換えているので、当然name
の値も書き変わるということである。
4つ目の記事では分かりやすい具体例が載っていました。
つまり、「破壊的な変更を加える処理をしたい場合はポインタを使う」という認識で良さそうです。
ポインタを使ってアドレスに変更を加えるため、元の場所の値を書き換えている、というイメージです。
元の場所の値を書き換えるため、メモリの使用量を減らせる(?)というメリットもあるみたいです。
次回
水曜日から金曜日までの3日間では、ConcurrencyのGoroutinesまでしか進みませんでした💧 進捗としては8割だと思うのですが、残りの2割がなかなか進まないです。
なかなかコードを読み解けなかったのと、Exerciseが個人的に難しかったのが進捗の遅い原因でした。既に修了した部分でも、まだ理解が曖昧なところは多いです。
また時間のあるときに、ConcurrencyのChannels以降をトライしてみたいと思います。💪
(もし、この記事の理解で間違っている箇所があれば、ご指摘頂けると嬉しいです!)
追記:A Tour of Goの解説動画と、Go入門資料
ネットで色々調べてみたのですが、こちらのShizuoka.goさんの解説動画がかなり良さそうです。
また、tenntennさんの作成したこちらのGo入門資料がとても良さそうです。
A Tour of Goは少し難しいかもですね。Shizuoka.goが解説どうが作ってるみたいなので、そのあたりも参考にすると良いかもしれません。一応、この辺に私も教材をまとめてますが、Microsoftのやつは割と良さそうです。https://t.co/BRPKwrODba
— tenntenn (@tenntenn) 2022年9月15日