Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19196] ファイルの概要

このコミットは、Go言語の公式ドキュメント「Effective Go」における、レシーバのポインタと値に関する記述の明確化と補足を行っています。具体的には、アドレス可能な値に対してポインタレシーバを持つメソッドを呼び出す際に、Goコンパイラが自動的にアドレス演算子を挿入するという「便利な例外」について追記されています。

コミット

commit f8f34c330c651b16ef8c54e60f4862b4a66b4a41
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Thu Apr 17 01:40:04 2014 -0400

    doc/effective_go: mention that b.Write is a shorthand for (&b).Write when b is addressable.
    
    The rewrite is due to Rob.
    
    LGTM=r, bradfitz, josharian
    R=golang-codereviews, bradfitz, r, josharian
    CC=golang-codereviews
    https://golang.org/cl/87410043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f8f34c330c651b16ef8c54e60f4862b4a66b4a41

元コミット内容

このコミットは、doc/effective_go ドキュメントにおいて、b がアドレス可能な場合に b.Write(&b).Write の短縮形であることを明記するものです。この書き換えは Rob Pike によるものです。

変更の背景

Go言語では、メソッドのレシーバが値型かポインタ型かによって、そのメソッドを呼び出せる型に厳密なルールがあります。一般的に、ポインタレシーバを持つメソッドはポインタ型の変数に対してのみ呼び出すことができ、値型の変数に対しては呼び出せません。これは、ポインタレシーバのメソッドがレシーバの値を変更する可能性があり、値のコピーに対して呼び出すと、その変更が元の値に反映されないという「間違い」を防ぐためです。

しかし、実際には多くのGoプログラマが、アドレス可能な値型の変数に対してポインタレシーバのメソッドを直接呼び出すという慣用的な記述を行っていました。例えば、var b bytes.Buffer のような値型の変数 b に対して、bytes.Buffer のポインタレシーバを持つ Write メソッドを b.Write(...) のように呼び出すことが可能です。これはGoコンパイラが内部的に (&b).Write(...) と書き換えているためですが、この挙動がドキュメントで明示的に説明されていなかったため、Go言語の初心者にとっては混乱の原因となる可能性がありました。

このコミットは、この「便利な例外」を公式ドキュメント「Effective Go」に明記することで、Go言語のメソッド呼び出しルールに関する理解を深め、プログラマの混乱を解消することを目的としています。

前提知識の解説

このコミットの理解には、Go言語における以下の概念の理解が不可欠です。

1. メソッドとレシーバ

Go言語では、関数が特定の型に関連付けられると「メソッド」と呼ばれます。メソッドは、そのメソッドが操作する値(またはポインタ)を「レシーバ」として受け取ります。レシーバは、関数名の前に括弧で囲んで指定されます。

例:

type MyInt int

// 値レシーバのメソッド
func (i MyInt) Add(j int) MyInt {
    return i + MyInt(j)
}

// ポインタレシーバのメソッド
func (i *MyInt) Increment() {
    *i++
}

2. 値レシーバとポインタレシーバ

  • 値レシーバ (Value Receiver): メソッドがレシーバの「値のコピー」を受け取ります。メソッド内でレシーバの値を変更しても、元の値には影響しません。
  • ポインタレシーバ (Pointer Receiver): メソッドがレシーバの「ポインタ」を受け取ります。メソッド内でポインタが指す値を変更すると、元の値も変更されます。

3. メソッドセット (Method Set)

Go言語では、各型が持つメソッドの集合を「メソッドセット」と呼びます。インターフェースが満たされるかどうかは、このメソッドセットによって決定されます。

  • T のメソッドセットは、値レシーバを持つすべてのメソッドを含みます。
  • *T (ポインタ型) のメソッドセットは、値レシーバとポインタレシーバの両方を持つすべてのメソッドを含みます。

このルールにより、ポインタレシーバを持つメソッドは、通常、ポインタ型の変数に対してのみ呼び出すことができます。

4. アドレス可能性 (Addressability)

Go言語において、「アドレス可能 (addressable)」とは、その値がメモリ上の特定のアドレスを持つことを意味します。アドレス可能な値に対しては、& 演算子を使ってそのアドレス(ポインタ)を取得できます。

一般的に、変数、配列の要素、スライスの要素、構造体のフィールドなどはアドレス可能です。一方、マップの要素、文字列の要素、関数の戻り値、定数などはアドレス可能ではありません。

技術的詳細

このコミットが説明している技術的詳細は、Goコンパイラがメソッド呼び出しをどのように処理するか、特に「アドレス可能な値」に対するポインタレシーバのメソッド呼び出しの特殊なケースに焦点を当てています。

Go言語の仕様では、ポインタレシーバを持つメソッドは、そのレシーバのポインタ型に対してのみ呼び出すことができるとされています。例えば、type T struct { ... }func (t *T) M() { ... } という定義がある場合、var v T という値型の変数に対して v.M() を直接呼び出すことは、本来であれば許可されません。これは、M*T のメソッドセットに属し、T のメソッドセットには属さないためです。

しかし、Goコンパイラは、この厳密なルールに「便利な例外」を設けています。もしメソッド呼び出しのレシーバが「アドレス可能な値」であり、かつその値のポインタ型が呼び出そうとしているメソッドを持っている場合、コンパイラは自動的にレシーバのアドレスを取得し、ポインタレシーバのメソッドを呼び出すように書き換えます。

具体的には、v.M() のような呼び出しがあった場合、コンパイラは v がアドレス可能かどうかをチェックします。もしアドレス可能であれば、コンパイラは内部的にこの呼び出しを (&v).M() と解釈し、コンパイルします。これにより、プログラマは明示的にアドレス演算子 & を記述することなく、アドレス可能な値に対してポインタレシーバのメソッドを呼び出すことができます。

この挙動は、特に bytes.Buffer のような型でよく見られます。bytes.Buffer は構造体であり、その Write メソッドはポインタレシーバ (func (b *Buffer) Write(...)) を持ちます。

var b bytes.Buffer // b は値型の変数で、アドレス可能
b.Write([]byte("hello")) // コンパイラが (&b).Write([]byte("hello")) に書き換える

この自動的な書き換えは、コードの記述を簡潔にし、Go言語の柔軟性を高める一方で、その背後にあるメカニズムを理解していないと混乱を招く可能性がありました。今回のドキュメントの更新は、この重要な挙動を明示的に説明することで、Go言語の学習者や開発者にとっての明確性を向上させています。

コアとなるコードの変更箇所

変更は doc/effective_go.html ファイルに対して行われています。

--- a/doc/effective_go.html
+++ b/doc/effective_go.html
@@ -2056,10 +2056,22 @@ We pass the address of a <code>ByteSlice</code>
 because only <code>*ByteSlice</code> satisfies <code>io.Writer</code>.\n The rule about pointers vs. values for receivers is that value methods\n can be invoked on pointers and values, but pointer methods can only be\n-invoked on pointers.  This is because pointer methods can modify the\n-receiver; invoking them on a copy of the value would cause those\n-modifications to be discarded.\n+invoked on pointers.\n </p>\n+\n+<p>\n+This rule arises because pointer methods can modify the receiver; invoking\n+them on a value would cause the method to receive a copy of the value, so\n+any modifications would be discarded.\n+The language therefore disallows this mistake.\n+There is a handy exception, though. When the value is addressable, the\n+language takes care of the common case of invoking a pointer method on a\n+value by inserting the address operator automatically.\n+In our example, the variable <code>b</code> is addressable, so we can call\n+its <code>Write</code> method with just <code>b.Write</code>. The compiler\n+will rewrite that to <code>(&amp;b).Write</code> for us.\n+</p>\n+\n <p>\n By the way, the idea of using <code>Write</code> on a slice of bytes\n is central to the implementation of <code>bytes.Buffer</code>.\n```

## コアとなるコードの解説

この変更は、`doc/effective_go.html` の `<p>` タグ内のテキストを修正・追記しています。

**削除された行:**
```html
-invoked on pointers.  This is because pointer methods can modify the
-receiver; invoking them on a copy of the value would cause those
-modifications to be discarded.

この部分は、ポインタレシーバのメソッドがポインタに対してのみ呼び出せる理由(値のコピーでは変更が破棄されるため)を説明していました。この説明自体は正しいのですが、後述の「便利な例外」を導入するために、より詳細な説明に置き換えられています。

追加された行:

+invoked on pointers.
+</p>
+
+<p>
+This rule arises because pointer methods can modify the receiver; invoking
+them on a value would cause the method to receive a copy of the value, so
+any modifications would be discarded.
+The language therefore disallows this mistake.
+There is a handy exception, though. When the value is addressable, the
+language takes care of the common case of invoking a pointer method on a
+value by inserting the address operator automatically.
+In our example, the variable <code>b</code> is addressable, so we can call
+its <code>Write</code> method with just <code>b.Write</code>. The compiler
+will rewrite that to <code>(&amp;b).Write</code> for us.
+</p>

新しく追加された段落では、まずポインタレシーバのメソッドを値に対して呼び出すことがなぜ間違いであるか(変更が破棄されるため)を再確認しています。その上で、重要な「便利な例外 (handy exception)」について説明しています。

この例外とは、値がアドレス可能である場合、Go言語はポインタレシーバのメソッドを値に対して呼び出すという一般的なケースを自動的に処理し、アドレス演算子を自動的に挿入するというものです。例として、bytes.Bufferb 変数(アドレス可能)に対して b.Write を呼び出すと、コンパイラが自動的に (&b).Write に書き換えることが明記されています。

この変更により、「Effective Go」ドキュメントは、Go言語のメソッド呼び出しに関するより完全で正確な情報を提供するようになりました。

関連リンク

参考にした情報源リンク