LoginSignup
20
13

More than 3 years have passed since last update.

これさえ見ればGoのポインタがわかる!というリンク集

Last updated at Posted at 2019-12-19

Go7 Advent Calendar 2019 の 20 日目の記事です。
昨日見たらぽっかり空いていたので埋めます。


皆さんは値とポインタを適切に使い分けることができているでしょうか?
基礎的なところなのでちゃんと理解しておきたいものですね。

私はたまに使うと細かなところを時々忘れていて、毎回資料探しからやり直すことになって面倒でした。
そこで、わかりやすかったページのリンクをここにまとめておくことにしました。

公式ドキュメントの翻訳も載せていますので、よろしければブックマークしてお使いください。

そもそもポインタがわからない方

まずは基礎全般を理解するのが良いと思います。


入門用のサイトや書籍で学びましょう。
(クリックで折り畳みを開閉)

公式資料

公式のドキュメントがやはり基本ですね。


公式 Wiki: Receiver Type の部分
(クリックで翻訳文を開閉)

レシーバの型(⇒ 原文

メソッドのレシーバに値とポインタのどちらを使うか選ぶのは難しいものです。新米の Go プログラマには特に難しいです。もし迷ったらポインタを使いましょう。しかし、値レシーバのほうが適していることもあります。その理由としてよくあるのは効率性で、例えば変更することのない小さな構造体や基本型の値なら値レシーバのほうが効率的です。ガイドラインとして役立つものをいくつか挙げます。


  • レシーバがマップか関数かチャネルなら、ポインタを使わないこと。 レシーバがスライスで、そのメソッドが再スライスや再アロケートをしないなら、ポインタを使わないこと。
  • メソッドでレシーバが変更されるなら、そのレシーバはポインタにすること。
  • レシーバが構造体で、sync.Mutex かそれに近い同期するフィールドを含んでいるなら、コピーを避けるためにレシーバをポインタにすること。
  • レシーバが大きな構造体か配列なら、ポインタレシーバのほうが効率的です。「大きな」とはどれくらいのことでしょうか。その要素全てを引数としてメソッドに渡すと考えたときに大きすぎると感じるなら、レシーバとしても大きすぎます。
  • 関数かメソッドが並行実行されるとき、あるいはそのメソッドから呼び出されるときに、レシーバが変更されることはありますか? 値型の場合、メソッドが呼ばれるとレシーバのコピーが作られるため、メソッドの外で起こった更新はそのレシーバに適用されません。 変更を元のレシーバにも適用する必要があれば、そのレシーバをポインタにすること。
  • レシーバが構造体か配列かスライスで、その要素が変化し得るものを指すポインタであれば、ポインタレシーバのほうがコードの読み手にも意図が明確になるので好ましいです。
  • レシーバが性質的に値型である小さな配列か構造体(例えば time.Time 型のようなもの)であって、フィールドがイミュータブルでポインタも持たないもの、あるいは intstring のようなシンプルな基本型であれば、値レシーバが適しています。 値レシーバなら、生じ得るガーベジの量を減らすことができます。 もし値メソッドに値が渡されると、ヒープへの割り当てではなくスタックへのコピーが行われるからです。 (ただし、コンパイラはこのヒープの割り当てを避けようとはするものの、うまくいくとは限りません。) プロファイルもしないままこの理由で値レシーバ型を選ばないこと。

最後に。もし迷ったらポインタレシーバを使いましょう。


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 の実装の根幹となっています。

引数・レシーバ等での使い分け

スライス

  • Go のスライスでハマッたところ - Block Rockin’ Codes
    • 渡した関数側で append() して lencap が変わると元のスライスに反映されないという挙動について理解しやすいです。
    • スライスはポインタ渡しする必要がないという説明を時々目にしますが、必要なときもあることがわかります。

構造体

構造体に関するテクニック

ポインタのポインタ

パフォーマンス、安全性など

理解度チェック

理解できたと思ったら試してみてください。3


  1. リストに含まれていないものですが、私は最初に「スターティングGo言語」という本で学びました。基本的な文法が網羅されていて良かったので、新版が出ていないのが残念です。 

  2. Go言語でinterfaceをimpleしてるつもりが「does not implement (method has pointer receiver)」って叱られる【golang】【pointer】【ダックタイピング】 - DRYな備忘録
    ポインタレシーバのメソッドにはポインタを使うルールはこちらの記事でもわかりやすいです。 

  3. Big Sky :: Go のポインタの躓きやすい点
    こちら(後半部分)も理解度チェックになりそうですが、少し難しいので脚注にしました。こういう躓きがなくなるところまで理解したいものです。 

20
13
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
20
13