Go7 Advent Calendar 2019 の 20 日目の記事です。
昨日見たらぽっかり空いていたので埋めます。
皆さんは値とポインタを適切に使い分けることができているでしょうか?
基礎的なところなのでちゃんと理解しておきたいものですね。
私はたまに使うと細かなところを時々忘れていて、毎回資料探しからやり直すことになって面倒でした。
そこで、わかりやすかったページのリンクをここにまとめておくことにしました。
公式ドキュメントの翻訳も載せていますので、よろしければブックマークしてお使いください。
そもそもポインタがわからない方
まずは基礎全般を理解するのが良いと思います。
入門用のサイトや書籍で学びましょう。 (クリックで折り畳みを開閉)
-
- 特に関連するのは「More types: structs, slices, and maps.」のページです。
公式資料
公式のドキュメントがやはり基本ですね。
公式 Wiki: Receiver Type の部分(クリックで翻訳文を開閉)
レシーバの型(⇒ 原文)
メソッドのレシーバに値とポインタのどちらを使うか選ぶのは難しいものです。新米の Go プログラマには特に難しいです。もし迷ったらポインタを使いましょう。しかし、値レシーバのほうが適していることもあります。その理由としてよくあるのは効率性で、例えば変更することのない小さな構造体や基本型の値なら値レシーバのほうが効率的です。ガイドラインとして役立つものをいくつか挙げます。
- レシーバがマップか関数かチャネルなら、ポインタを使わないこと。
レシーバがスライスで、そのメソッドが再スライスや再アロケートをしないなら、ポインタを使わないこと。- メソッドでレシーバが変更されるなら、そのレシーバはポインタにすること。
- レシーバが構造体で、
sync.Mutex
かそれに近い同期するフィールドを含んでいるなら、コピーを避けるためにレシーバをポインタにすること。- レシーバが大きな構造体か配列なら、ポインタレシーバのほうが効率的です。「大きな」とはどれくらいのことでしょうか。その要素全てを引数としてメソッドに渡すと考えたときに大きすぎると感じるなら、レシーバとしても大きすぎます。
- 関数かメソッドが並行実行されるとき、あるいはそのメソッドから呼び出されるときに、レシーバが変更されることはありますか?
値型の場合、メソッドが呼ばれるとレシーバのコピーが作られるため、メソッドの外で起こった更新はそのレシーバに適用されません。
変更を元のレシーバにも適用する必要があれば、そのレシーバをポインタにすること。- レシーバが構造体か配列かスライスで、その要素が変化し得るものを指すポインタであれば、ポインタレシーバのほうがコードの読み手にも意図が明確になるので好ましいです。
- レシーバが性質的に値型である小さな配列か構造体(例えば
time.Time
型のようなもの)であって、フィールドがイミュータブルでポインタも持たないもの、あるいはint
やstring
のようなシンプルな基本型であれば、値レシーバが適しています。
値レシーバなら、生じ得るガーベジの量を減らすことができます。
もし値メソッドに値が渡されると、ヒープへの割り当てではなくスタックへのコピーが行われるからです。
(ただし、コンパイラはこのヒープの割り当てを避けようとはするものの、うまくいくとは限りません。)
プロファイルもしないままこの理由で値レシーバ型を選ばないこと。最後に。もし迷ったらポインタレシーバを使いましょう。
Effective Go: Pointers vs. Values の部分(クリックで翻訳文を開閉)
ポインタ vs 値(⇒ 原文)
ByteSize の例 で見たように、名前の付いたどんな型(ポインタ型と interface 型は除く)にもメソッドを定義することができます。
レシーバは構造体である必要はありません。上述のスライスに関する議論において、Append という関数について書きました。
それを関数としてではなくスライスが持つメソッドとしても定義できます。
そのためには、メソッドと紐付けることができるものとして名前付きの型をまず宣言しておき、その型の値をメソッドのレシーバとして使います。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 のアドレスを渡しているのは
*ByteSlice
だけがio.Writer
を満たすからです。
値メソッドはポインタでも値でも呼び出せるのに対し、ポインタメソッドはポインタでしか呼び出せないというのが、レシーバの「ポインタ vs 値」のルールです。2このルールは、ポインタメソッドならレシーバを変更できることによるものです。
値で呼び出したメソッドでは値のコピーを受け取るため、変更しても全てなかったことになります。
そのため、このような誤用は言語によって許容されていません。
しかし例外として便利なルールがあります。
値でポインタメソッドを呼び出すというよくあるケースにおいて、その値がアドレス可能であれば言語がアドレス演算子(&)を自動的に差し込んでケアしてくれます。
上の例では、b はアドレス可能な変数なので、単にb.Write
という形でWrite
メソッドを呼び出すことができます。
コンパイラがそれを(&b).Write
に書き換えてくれます。ちなみに、bytes のスライスで 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 のポインタの躓きやすい点
こちら(後半部分)も理解度チェックになりそうですが、少し難しいので脚注にしました。こういう躓きがなくなるところまで理解したいものです。 ↩