Go 语言类型系统

值接收者和指针接收者

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

Copy
type Animal struct {
name string
}

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

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

值接收者方法不管调用者是指类型还是指针类型，传递的始终是调用者的副本，方法内部不会对调用者本身做修改：

Copy
// 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
}

指针接收者方法传递的是调用者的引用，所以可以对调用者做修改：

Copy
// 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》

内置类型：数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型。因此，当对这些值进行增加或者删除的时候，会创建一个新值。基于这个结论，当把这些类型的值传递给方法或者函数时，应该传递一个对应值的副本。

原始类型：切片、映射、通道、接口和函数类型。后边没看懂 😅

结构类型：用来描述一组值。遵循如有修改就按指针传递。如果类型的值具备非原始的本质，就应该被共享，而不是被复制。即使方法没有修改接收者的值，依然要用指针接收者来声明的。

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

接口

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

Copy
// 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()
}

Copy
// 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 类型的值无法实现该接口，需要先了解方法集。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值，还是关联到指针，还是两个都关联。

规范描述的方法集:

values | methods receivers
T | (t T)
* T | (t T) and (t * T)

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

接收者的角度来看方法集规范:

methods receivers | values
(t T) | T and * T
(t * T) | * T

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

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

嵌入类型

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

Copy
type Cat struct {
Animal
color string
}

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

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

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

参考

《Go 语言实战》第五章

值接收者和指针接收者的区别 | Go 程序员面试笔试宝典