Go7 Advent Calendar 2019 の 20 日目の記事です。
昨日見たらぽっかり空いていたので埋めます。
皆さんは値とポインタを適切に使い分けることができているでしょうか?
基礎的なところなのでちゃんと理解しておきたいものですね。
私はたまに使うと細かなところを時々忘れていて、毎回資料探しからやり直すことになって面倒でした。
そこで、わかりやすかったページのリンクをここにまとめておくことにしました。
公式ドキュメントの翻訳も載せていますので、よろしければブックマークしてお使いください。
そもそもポインタがわからない方
まずは基礎全般を理解するのが良いと思います。
公式資料
公式のドキュメントがやはり基本ですね。
レシーバの型(⇒ 原文) メソッドのレシーバに値とポインタのどちらを使うか選ぶのは難しいものです。新米の Go プログラマには特に難しいです。もし迷ったらポインタを使いましょう。しかし、値レシーバのほうが適していることもあります。その理由としてよくあるのは効率性で、例えば変更することのない小さな構造体や基本型の値なら値レシーバのほうが効率的です。ガイドラインとして役立つものをいくつか挙げます。 最後に。もし迷ったらポインタレシーバを使いましょう。
公式 Wiki: Receiver Type の部分
(クリックで翻訳文を開閉)
sync.Mutex
かそれに近い同期するフィールドを含んでいるなら、コピーを避けるためにレシーバをポインタにすること。time.Time
型のようなもの)であって、フィールドがイミュータブルでポインタも持たないもの、あるいは int
や string
のようなシンプルな基本型であれば、値レシーバが適しています。
値レシーバなら、生じ得るガーベジの量を減らすことができます。
もし値メソッドに値が渡されると、ヒープへの割り当てではなくスタックへのコピーが行われるからです。
(ただし、コンパイラはこのヒープの割り当てを避けようとはするものの、うまくいくとは限りません。)
プロファイルもしないままこの理由で値レシーバ型を選ばないこと。
ポインタ vs 値(⇒ 原文) ByteSize の例 で見たように、名前の付いたどんな型(ポインタ型と interface 型は除く)にもメソッドを定義することができます。 上述のスライスに関する議論において、Append という関数について書きました。 このようにしても、更新されたスライスをメソッドから返さなければならない点は変わりません。 実は更に改善できます。 そうすると ByteSlice のアドレスを渡しているのは このルールは、ポインタメソッドならレシーバを変更できることによるものです。 ちなみに、bytes のスライスで Write を使えるという考えは
Effective Go: Pointers vs. Values の部分
(クリックで翻訳文を開閉)
レシーバは構造体である必要はありません。
それを関数としてではなくスライスが持つメソッドとしても定義できます。
そのためには、メソッドと紐付けることができるものとして名前付きの型をまず宣言しておき、その型の値をメソッドのレシーバとして使います。type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// ここの中身は上記のAppendの関数とまったく同じ
}
その使いにくさを解消するには、ByteSlice のポインタをレシーバとして取るようにメソッドを再定義し、呼び出し元のスライスをメソッドで上書きできるようにします。func (p *ByteSlice) Append(data []byte) {
slice := *p
// ここの中身は上記と同様だがreturnは無し
*p = slice
}
上の関数を、次のように標準の Write
メソッドと同じ見た目となるよう変更します。func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// ここも上記と同様
*p = slice
return len(data), nil
}
*ByteSlice
型は標準の io.Writer
インタフェースを満たして便利になります。
例えば、その型に print して書き込むことができます。 var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
*ByteSlice
だけが io.Writer
を満たすからです。
値メソッドはポインタでも値でも呼び出せるのに対し、ポインタメソッドはポインタでしか呼び出せないというのが、レシーバの「ポインタ vs 値」のルールです。2
値で呼び出したメソッドでは値のコピーを受け取るため、変更しても全てなかったことになります。
そのため、このような誤用は言語によって許容されていません。
しかし例外として便利なルールがあります。
値でポインタメソッドを呼び出すというよくあるケースにおいて、その値がアドレス可能であれば言語がアドレス演算子(&)を自動的に差し込んでケアしてくれます。
上の例では、b はアドレス可能な変数なので、単に b.Write
という形で Write
メソッドを呼び出すことができます。
コンパイラがそれを (&b).Write
に書き換えてくれます。bytes.Buffer
の実装の根幹となっています。
引数・レシーバ等での使い分け
-
golang の 引数、戻り値、レシーバをポインタにすべきか、値にすべきかの判断基準について迷っている - pospomeのプログラミング日記
- 公式 Wiki の説明(上記の Receiver Type の部分)を解説したような記事です。
-
Go 言語の値レシーバとポインタレシーバ | Step by Step
- 特にお勧めしたい記事です。
-
func (p Person) Greet(msg string)
とfunc Person.Greet(p Person, msg string)
は等価だといった仕組みの話もあって大変わかりやすいです。
スライス
-
Go のスライスでハマッたところ - Block Rockin’ Codes
- 渡した関数側で
append()
してlen
やcap
が変わると元のスライスに反映されないという挙動について理解しやすいです。 - スライスはポインタ渡しする必要がないという説明を時々目にしますが、必要なときもあることがわかります。
- 渡した関数側で
構造体
-
Golang で struct を扱う時に nil プロパティを扱いたい – II
- フィールドを値にすると空でも各型のゼロ値(int なら
0
、string なら""
など)になってしまいますが、ポインタにすればnil
になって区別できます。
- フィールドを値にすると空でも各型のゼロ値(int なら
構造体に関するテクニック
-
Big Sky :: Go 言語の struct の実体を引数で(なるべく)渡せない様にするテクニック
- ポインタで扱うべき構造体であっても、知らずに値で関数に渡してしまうことがあり得ます。それを防ぎやすくする(コピーしようとしていることに気づきやすくする)テクニックが紹介されています。
ポインタのポインタ
-
Goのポインタのポインタ - Qiita
- 複雑ですが、ここの図を見るとわかりやすいです。
パフォーマンス、安全性など
-
najeira: Go言語のスタックとヒープ
- 公式 Wiki の説明(上記の Receiver Type の部分)にも書かれている仕組みについて実際に検証されていてわかりやすいです。
理解度チェック
理解できたと思ったら試してみてください。3
-
Go理解度チェック - Google スライド
- やってみたら難しくて怖さが増しましたが、クイズのように楽しみながら確認できるのはいいですね。
-
リストに含まれていないものですが、私は最初に「スターティングGo言語」という本で学びました。基本的な文法が網羅されていて良かったので、新版が出ていないのが残念です。 ↩
-
Go言語でinterfaceをimpleしてるつもりが「does not implement (method has pointer receiver)」って叱られる【golang】【pointer】【ダックタイピング】 - DRYな備忘録
ポインタレシーバのメソッドにはポインタを使うルールはこちらの記事でもわかりやすいです。 ↩ -
Big Sky :: Go のポインタの躓きやすい点
こちら(後半部分)も理解度チェックになりそうですが、少し難しいので脚注にしました。こういう躓きがなくなるところまで理解したいものです。 ↩