Golang 指针接收器与值接收器

Published: 2023-03-02

Tags: Golang

本文总阅读量

本篇文章整理值接收器与指针接收器相关知识。

方法

Golang 方法能给用户自定义的类型添加新的行为,它和函数的区别在于方法跟类型相关联,有一个特殊的 receiver 参数。

type Dog struct {
    Name string
}

// 方法
func (d Dog) GetName() string {
    return d.Name
}

// 函数
func GetName(d Dog) string {
    return d.Name
}

“接收器”名词解释(receiver)

type Rectangle struct {
    width  float64
    height float64
}

// 值接收器方法
func (r Rectangle) Area() float64 {
    return r.width * r.height
}

// 指针接收器方法
func (r *Rectangle) Scale(factor float64) {
    r.width *= factor
    r.height *= factor
}
  • 接收器:英文 receiver,是方法中的特殊参数,用于指代调用该方法的对象。
  • 值接收器:示例中 (r Rectangle) 这部分称为值接收器,它表明这个方法接收 Rectangle 类型。
  • 值接收器方法:示例中 func (r Rectangle) Area() float64 被称为值接收器方法,简称值方法。
  • 指针接收器:示例中 (r Rectangle) 这部分称为指针接收器,它表明这个方法接收 Rectangle 类型。
  • 指针接收器方法:示例中 func (r *Rectangle) Scale(s float64) 被称为指针接收器方法,简称指针方法。

本文将统一使用以上翻译。

修改结构体值需要使用指针接收器方法

值接收器代码示例

package main

import "fmt"

type Dog struct {
    Name string
}

func (d Dog) GetName() string {
    return d.Name
}

func (d Dog) SetName(name string) {
    d.Name = name
}

func main() {

    d := Dog{Name: "Charlie"}
    fmt.Println(d.GetName())

    d.SetName("Luna")
    fmt.Println(d.GetName())
}

输出如下

Charlie
Charlie

调用值接收器方法会触发 Dog 结构体复制,对副本的修改不会改变 Dog 实例的值。

type Dog struct {
    Name string
}

func (d *Dog) GetName() string {
    return d.Name
}

func (d *Dog) SetName(name string) {
    d.Name = name
}

稍作修改,将接收器类型由 T 修改为 *T,输出内容就会符合我们的预期。

Charlie
Luna

指针接收器会触发复制指针,不过复制的指针也还是指向原数据,所以可以修改数据值。

值类型不能调用指针接收器方法

此处将 GetName 实现为值接收器方法,而将 SetName() 实现为指针接收器方法。(实际开发中应保持接收器类型相同,不应混用)

type Dog struct {
    Name string
}

func (d Dog) GetName() string {
    return d.Name
}

func (d *Dog) SetName(name string) {
    d.Name = name
}

func main() {

    Dog{Name: "Charlie"}.GetName() // ok

    Dog{Name: "Charlie"}.SetName("Luna") // 报错:cannot call pointer method SetName on Dog
}

Dog{Name: "Charlie"} 是一个值类型的结构体实例,在其上调用 SetName 方法就会触发报错,不能编译。

更详细的解释是它是右值表达式,还未存储到可寻址变量,只有当其赋值给变量存储在内存中时,才能对它进行寻址操作。

且看赋值后的代码如下:

func main() {

    dog := Dog{Name: "Charlie"}

    dog.GetName() // ok
    dog.SetName("Luna") // ok
}

为什么赋值到 dog 变量后,SetName 方法就能用了呢?这是因为在 可寻址值变量(左值) 上调用指针接收器方法,Golang 会自动隐式的为变量取地址后调用方法,Golang 的语法糖让我们不用再啰嗦的显式取地址。同理,当指针类型调用值接收器方法时,Golang 也会通过指针找到值类型,在值类型上调用方法。

// 值类型调用指针接收器方法
dog := Dog{Name: "Charlie"}
dog.SetName("Luna")   // 编译器会处理为 (&dog).SetName("Luna")

// 指针类型调用值接收器方法
dog := &Dog{Name: "Charlie"}
dog.GetName()   // 编译器会处理为 (*dog).SetName("Luna")

在官方 effective go 文档中,对两者区别有精确的描述:

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.

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.

意思是:

值方法(value methods)可以通过指针和值调用,但是指针方法(pointer methods)只能通过指针来调用。 但有一个例外,如果某个值是可寻址的(addressable,或者说左值),那么编译器会在值调用指针方法时自动插入取地址符,使得在此情形下看起来像指针方法也可以通过值来调用。

类型与接收器方法的调用关系

- 值接收器方法 指针接收器方法
值类型 在值的副本上调用方法 使用值的引用来调用方法,上例中,dog.SetName("Luna") 实际上是 (&dog).SetName("Luna")(语法糖)
指针类型 指针被解引用为值,上例中,dog.GetName() 实际上是 (*dog).SetName("Luna") 在指针的副本上调用方法,方法里的操作会影响到接收器

从逻辑上理解为什么 “值类型不能调用指针接收器方法”

指针接收器方法,很可能在方法中会对调用者的属性进行更改操作,从而影响接收器;而对于值接收器方法,在方法中不会对接收器本身产生影响。

所以,当实现了一个值接收器方法,就可以自动生成一个指针接收器方法,因为两者都不会影响接收器。但是,当实现了一个指针接收器方法,如果此时自动生成一个值接收器方法,原本期望对接收器的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响接收器。

可以简单记住下面这点:

如果实现了值接收器方法,会隐含地也实现了指针接收器方法。

补充:本段描述中的 “接收器” 可能不如 “调用者” 好理解,不过 “接收器” 是更规范的称呼。

Go 语言中的方法(method)是一种作用于特定类型变量的函数。 这种特定类型变量叫做接收器(receiver)。 如果将特定类型理解为结构体或类,那接收器的概念就类似于其他语言中的 this 或者 self,在 Go 语言中,接收器的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法,但必须是通过type定义的类型。

接收器方法与接口实现

Uber Go 语言编码规范中文版 - 接收器 (receiver) 与接口

一个类型可以有值接收器方法集和指针接收器方法集,值接收器方法集是指针接收器方法集的子集,反之不是

值对象只可以使用值接收器方法集,指针对象可以使用 值接收器方法集 + 指针接收器方法集

接口的匹配 (或者叫实现),类型实现了接口的所有方法,叫匹配,具体的讲,要么是类型的值方法集匹配接口,要么是指针方法集匹配接口,具体的匹配分两种:

值方法集和接口匹配:给接口变量赋值的不管是值还是指针对象,都 ok,因为都包含值方法集 指针方法集和接口匹配:只能将指针对象赋值给接口变量,因为只有指针方法集和接口匹配,如果将值对象赋值给接口变量,会在编译期报错 (会触发接口合理性检查机制)

有了之前的基础,从合理性角度就很好理解,如果我们实现了指针接收器方法和接口匹配,并且在方法中使用指针修改了接收器,如果允许将值类型赋值给接口变量,再通过接口变量调用方法,值类型和指针类型执行的结果就会不一致,这显然是不合理的。

其实只要记住:指针接收器能修改接收器值,而值接收器不行,问题的核心还是在 Golang 中的传参是传递值。

选择指针接收器还是值接收器?

在定义接收器为值还是指针时,主要有以下几个考虑点:

  • 方法是否需要修改接收器本身,如果该方法需要改变接收器,则接收器必须是指针。
  • 效率问题,如果接收器是值类型,那在方法调用时一定会产生值拷贝,而大对象拷贝代价很大。
  • 一致性,对于同一个的方法,不应混用值接收器方法和指针接收器方法。

使用值接收器的场景:不需要编辑接收器的值,依赖值接收器的并发安全,出于效率的原因,例如小的不变结构或基本类型的值。

遇事不决,选择使用指针接收器。

最后的结论

除去 Golang 的语法糖,值类型不能调用指针接收器方法,指针类型既可以调用值接收器方法也可以调用指针接收器方法。

参考