xChar

值接收者和指针接收者

指定了接收者的函数称为方法。其中接收者可以是值接收者Animal, 也可以是指针接收者 *Animal

type Animal struct {
	name string
}

func (a Animal) valueFn() {} // 值接收者
func (a *Animal) ptrFn() {} // 指针接收者

在调用方法的时候,调用者可以是该类型的值类型也可以是指针类型。

  • 值接收者方法不管调用者是指类型还是指针类型,传递的始终是调用者的副本,方法内部不会对调用者本身做修改:
// eg1.
func (a Animal) valueFn() {
	a.name = "new_" + a.name
}
func main() {
	a := Animal{"cat"} // 值类型
	a.valueFn()
	println("this is name", a.name)
	// => this is name cat
	
	b := &Animal{"dog"} // 指针类型
	b.valueFn()
	println("this is name", b.name)
	// => this is name dog
}
  • 指针接收者方法传递的是调用者的引用,所以可以对调用者做修改:
// eg2.
func (a *Animal) ptrFn() {
	a.name = "new_" + a.name
}
func main() {
	a := Animal{"cat"} // 值类型
	a.ptrFn()
	println("this is name", a.name)
	// => this is name new_cat
	
	b := &Animal{"dog"} // 指针类型
	b.ptrFn()
	println("this is name", b.name)
	// => this is name new_dog
}

这背后是编译器做了一些工作,如下表:

-值接收者指针接收者
值类型调用者方法会使用调用者的一个副本,类似于“传值”使用值的引用来调用方法,eg2 实际上是 (&a).ptrFn()
指针类型调用者指针被解引用为值,eg1 实际上是 (*b).valueFn()实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针

类型的本质

在声明一个新类型之后,声明一个该类型的方法之前,需要先回答一个问题:这个类型的本质是什么。如果给这个类型增加或者删除某个值,是要创建一个新值,还是要更改当前的值?如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。这个答案也会影响程序内部传递这个类型的值的方式:是按值做传递,还是按指针做传递。保持传递的一致性很重要。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。

是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。
—— 《Go 语言实战 5.3》

  • 内置类型:数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型。因此,当对这些值进行增加或者删除的时候,会创建一个新值。基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。
  • 原始类型:切片、映射、通道、接口和函数类型。后边没看懂 😅
  • 结构类型:用来描述一组值。遵循如有修改就按指针传递。如果类型的值具备非原始的本质,就应该被共享,而不是被复制。即使方法没有修改接收者的值,依然要用指针接收者来声明的。

这一节没有看明白,大概是说对于非结构类型,应当参考标准库的习惯;对于结构类型,一般情况下如果不需要对调用者做修改,就按值传递,如果需要修改则按指针传递。

接口

虽然对不同类型的调用者,值接收者和指针接收者方法都可以调用,但在接口的实现上略有不同,实现接收者是值类型的方法,会隐式地实现了接收者是指针类型的方法,而实现接收者是指针类型的方法时,不会自动实现接收者是值类型的方法。

// eg3. 编译通过
type IAnimal interface {
	valueFn()
	ptrFn()
}

func (a Animal) valueFn() {}
func (a *Animal) ptrFn() {}

func main() {
    var a IAnimal = &Animal{"cat"}
	a.valueFn()
	a.ptrFn()
}
// eg4. 编译失败
func main() {
    var a IAnimal = Animal{"cat"} // 改为值类型
	a.valueFn()
	a.ptrFn()
}
// => .\main.go:22:18: cannot use Animal{…} (value of type Animal) as IAnimal value in variable declaration: Animal does not implement IAnimal (method ptrFn has pointer receiver)

方法集

要了解用指针接收者来实现接口时为什么 user 类型的值无法实现该接口,需要先了解方法集。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。

  • 规范描述的方法集:
valuesmethods receivers
T(t T)
* T(t T) and (t * T)

意即 T 类型的值的方法集只包含值接收者声明的方法。而指向 T 类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。

  • 接收者的角度来看方法集规范:
methods receiversvalues
(t T)T and * T
(t * T)* T

这个视角是说,如果使用值接收者来实现一个接口,值类型值和指针都能够实现对应的接口。如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。

所以 eg4 中的值类型 Animal 会提示没有实现接口 IAnimal

嵌入类型

嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。

type Cat struct {
	Animal
	color string
}

func main() {
	c := Cat{Animal{"miao"},"yellow"}
	c.Animal.valueFn()
}

内部类型的属性和方法也可以被提升到外部类型直接访问:

func main() {
	//...
	c.valueFn()
	println("this is c name", c.name)
}

参考

Loading comments...