Go 挖坑指南: “cannot take the address of XXX” and “cannot call pointer method on XXX”

先看代码

package main

type B struct {
    Id int
}

func New() B {
    return B{}
}

func New2() *B {
    return &B{}
}

func (b *B) Hello() {
    return
}

func (b B) World() {
    return
}

func main() {
    // 方法的接收器为 *T 类型
    New().Hello() // 编译不通过

    b1 := New()
    b1.Hello() // 编译通过

    b2 := B{}
    b2.Hello() // 编译通过

    (B{}).Hello() // 编译不通过
    B{}.Hello()   // 编译不通过

    New2().Hello() // 编译通过

    b3 := New2()
    b3.Hello() // 编译通过

    b4 := &B{} // 编译通过
    b4.Hello() // 编译通过

    (&B{}).Hello() // 编译通过

    // 方法的接收器为 T 类型
    New().World() // 编译通过

    b5 := New()
    b5.World() // 编译通过

    b6 := B{}
    b6.World() // 编译通过

    (B{}).World() // 编译通过
    B{}.World()   // 编译通过

    New2().World() // 编译通过

    b7 := New2()
    b7.World() // 编译通过

    b8 := &B{} // 编译通过
    b8.World() // 编译通过

    (&B{}).World() // 编译通过
}

输出结果

./main.go:25:10: cannot call pointer method on New()
./main.go:25:10: cannot take the address of New()
./main.go:33:10: cannot call pointer method on B literal
./main.go:33:10: cannot take the address of B literal
./main.go:34:8: cannot call pointer method on B literal
./main.go:34:8: cannot take the address of B literal

问题总结

假设 T
类型的方法上接收器既有 T
类型的,又有 *T
指针类型的,那么就不可以在不能寻址的 T
值上调用 *T
接收器的方法

&B{}
B{}
b := B{}

延伸思考

Go 语言规范
中规定了可寻址(addressable)对象的定义:

For an operand x of type T, the address operation &x generates a pointer of type *T to x. The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array. As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal. If the evaluation of x would cause a run-time panic, then the evaluation of &x does too.

对于类型为 T
的操作数 x
,地址操作符 &x
将生成一个类型为 *T
的指针指向 x
。操作数必须可寻址,即,变量、间接指针、切片索引操作,或可寻址结构体的字段选择器,或可寻址数组的数组索引操作。作为可寻址性要求的例外, x
也可为(圆括号括起来的)复合字面量。如果对 x
的求值会引起运行时恐慌,那么对 &x
的求值也会引起恐慌。
For an operand x of pointer type *T, the pointer indirection *x denotes the variable of type T pointed to by x. If x is nil, an attempt to evaluate *x will cause a run-time panic.

对于指针类型为 *T
的操作数 x
,间接指针 *x
表示类型为 T
的值指向 x
。若 x
nil
,尝试求值 *x
将会引发运行时恐慌。
以下几种是可寻址的:

&x
&*x
&s[1]
&point.X
&a[0]
&struct{ X int }{1}

下列情况 x
是不可以寻址的,不能使用 &x
取得指针:

  • 字符串中的字节
  • map 对象中的元素
  • 接口对象的动态值(通过 type assertions 获得)
  • 常数
  • literal 值(非 composite literal)
  • package 级别的函数
  • 方法 method(用作函数值)
  • 中间值(intermediate value):

    • 函数调用
    • 显式类型转换
    • 各种类型的操作(除了指针引用 pointer dereference 操作 *x
      ):

      • channel receive operations
      • sub-string operations
      • sub-slice operations
      • 加减乘除等运算符

有几个点需要解释下:

  • 常数为什么不可以寻址?
    > 如果可以寻址的话,我们可以通过指针修改常数的值,破坏了常数的定义。
  • map 的元素为什么不可以寻址?
    > 两个原因,如果对象不存在,则返回零值,零值是不可变对象,所以不能寻址,如果对象存在,因为 Go 中 map 实现中元素的地址是变化的,这意味着寻址的结果是无意义的。
  • 为什么 slice 不管是否可寻址,它的元素读是可以寻址的?
    > 因为 slice 底层实现了一个数组,它是可以寻址的。
  • 为什么字符串中的字符/字节又不能寻址呢?
    > 因为字符串是不可变的。

规范中还有几处提到了 addressable:

  • 调用一个接收者为指针类型的方法时,使用一个可寻址的值将自动获取这个值的指针
  • ++
    --
    语句的操作对象必须可寻址或者是 map 的索引操作
  • 赋值语句 =
    的左边对象必须可寻址,或者是 map 的索引操作,或者是 _
  • 上条同样使用 for ... range
    语句

参考资料