banner
魔法少女秋小日

Qiuxiaori

github
email

Go 语言类型系统

值接收者和指针接收者#

指定了接收者的函数称为方法。其中接收者可以是值接收者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)
}

参考#

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。