函数和结构体的区别

函数是独立的程序实体。我们可以声明有名字的函数,也可以声明没有名字的函数,还可以把函数当做普通的值来传递。我们还可以把具有相同签名的函数抽象成独立的函数类型。

方法需要有名字,不能被当做值来看待,最重要的是:方法必须隶属于某一个类型。方法所述的类型会通过方法声明中的接收者(receiver)声明体现出来。

方法声明

func (接收者名称 接收者类型) methodName(参数列表)(结果列表)

举个例子

// AnimalCategory 代表动物分类学中的基本分类法。
type AnimalCategory struct {
	kingdom string // 界。
	phylum string // 门。
	class  string // 纲。
	order  string // 目。
	family string // 科。
	genus  string // 属。
	species string // 种。
}

func (ac AnimalCategory) String() string {
	return fmt.Sprintf("%s%s%s%s%s%s%s",ac.kingdom, ac.phylum, ac.class, ac.order,ac.family, ac.genus, ac.species)
}

func main {
	category := AnimalCategory{species: "cat"}
	fmt.Printf("The animal category: %s\n", category)
}

从String方法的声明中可以得知,String方法隶属于AnimalCategory类型。

调用fmt.Printf函数时,使用占位符%s和category值本身就可以打印出category的字符串表示形式,无需显示调用category的String方法。fmt.Printf函数会自动就寻找String函数。即使一个类型没有实现String方法,fmt.Printf也能按照默认的格式打印出该类型的字符串表示。

一个数据类型关联的所有方法,共同组成了该类型的方法集合。同一个方法集合中的方法不能出现重名。并且,如果它们所属的是一个结构体类型,那么它们的名称与该类型中任何字段的名称也不能重复。

我们可以把结构体类型中的一个字段看作是它的一个属性或者一项数据,再把隶属于它的一个方法看作是附加在其中数据之上的一个能力或者一项操作。将属性及其能力(或者说数据及其操作)封装在一起,是面向对象编程(object-oriented programming)的一个主要原则。

Go 语言摄取了面向对象编程中的很多优秀特性,同时也推荐这种封装的做法。从这方面看,Go语言其实是支持面向对象编程的,但它选择摒弃了一些在实际运用过程中容易引起程序开发者困惑的特性和规则。

匿名字段

type Animal struct {
	scientificName string // 学名。
	AnimalCategory    // 动物基本分类。
}

Go语言规范规定,如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。我们可以通过此类型变量的名称后跟“.”,再后跟嵌入字段类型的方式引用到该字段。也就是说,嵌入字段的类型既是类型也是名称。

在某个代表变量的标识符的右边加“.”,再加上字段名或方法名的表达式被称为选择表达式,它用来表示选择了该变量的某个字段或者方法。

嵌入字段的方法集合会被无条件地合并进被嵌入类型的方法集合中。

只要名称相同,无论这两个方法的签名是否一致,被嵌入类型的方法都会“屏蔽”掉嵌入字段的同名方法。

类似的,由于我们同样可以像访问被嵌入类型的字段那样,直接访问嵌入字段的字段,所以如果这两个结构体类型里存在同名的字段,那么嵌入字段中的那个字段一定会被“屏蔽”

多层嵌入的问题

在这种情况下,“屏蔽”现象会以嵌入的层级为依据,嵌入层级越深的字段或方法越可能被“屏蔽”。

如果处于同一个层级的多个嵌入字段拥有同名的字段或方法,那么从被嵌入类型的值那里,选择此名称的时候就会引发一个编译错误,因为编译器无法确定被选择的成员到底是哪一个

Go 语言是用嵌入字段实现了继承吗?

强调一下,Go 语言中根本没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之间的组合。

类型之间的组合采用的是非声明的方式,我们不需要显式地声明某个类型实现了某个接口,或者一个类型继承了另一个类型。

类型组合也是非侵入式的,它不会破坏类型的封装或加重类型之间的耦合。我们要做的只是把类型当做字段嵌入进来,然后坐享其成地使用嵌入字段所拥有的一切。如果嵌入字段有哪里不合心意,我们还可以用“包装”或“屏蔽”的方式去调整和优化。

类型间的组合也是灵活的,我们总是可以通过嵌入字段的方式把一个类型的属性和能力“嫁接”给另一个类型。这时候,被嵌入类型也就自然而然地实现了嵌入字段所实现的接口。

组合要比继承更加简洁和清晰,Go 语言可以轻而易举地通过嵌入多个字段来实现功能强大的类型,却不会有多重继承那样复杂的层次结构和可观的管理成本。

接口类型之间也可以组合。在 Go 语言中,接口类型之间的组合甚至更加常见,我们常常以此来扩展接口定义的行为或者标记接口的特征。

值方法和指针方法都是什么意思?有什么区别?

方法的接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指针类型。所谓的值方法,就是接收者类型是非指针的自定义数据类型的方法。

func (cat *Cat) SetName(name string) {
	cat.name = name
}

func (cat Cat) SetNameOfCopy(name string) {
	cat.name = name
}

方法setName的接收者类型是Cat。表示的Cat类型的指针类型。这时,Cat可以叫做Cat的基本类型。指针类型的值表示的是指向某个基本类型值的指针。

值方法和指针方法之间有什么不同点呢?

值方法的接收者是该方法所属的那个类型值的一个副本。我们在该方法内对该副本的修改一般都不会体现在原值上,除非这个类型本身是某个引用类型(比如切片或字典)的别名类型。

而指针方法的接收者,是该方法所属的那个基本类型值的指针值的一个副本。我们在这样的方法内对该副本指向的值进行修改,却一定会体现在原值上。

一个自定义数据类型的方法集合中仅会包含它的所有值方法,而该类型的指针类型的方法集合却囊括了前者的所有方法,包括所有值方法和所有指针方法

我们在这样的基本类型的值上只能调用到它的值方法。但是,Go 语言会适时地为我们进行自动地转译,使得我们在这样的值上也能调用到它的指针方法。

如果一个基本类型和它的指针类型的方法集合是不同的,那么它们具体实现的接口类型的数量就也会有差异,除非这两个数量都是零。

以上结论可以结合下面的例子进行体会。

package main

import "fmt"

type Cat struct {
	name           string // 名字。
	scientificName string // 学名。
	category       string // 动物学基本分类。
}

func New(name, scientificName, category string) Cat {
	return Cat{
		name:           name,
		scientificName: scientificName,
		category:       category,
	}
}

func (cat *Cat) SetName(name string) {
	cat.name = name
}

func (cat Cat) SetNameOfCopy(name string) {
	cat.name = name
}

func (cat Cat) Name() string {
	return cat.name
}

func (cat Cat) ScientificName() string {
	return cat.scientificName
}

func (cat Cat) Category() string {
	return cat.category
}

func (cat Cat) String() string {
	return fmt.Sprintf("%s (category: %s, name: %q)",
		cat.scientificName, cat.category, cat.name)
}

func main() {
	cat := New("little pig", "American Shorthair", "cat")
	cat.SetName("monster") // (&cat).SetName("monster")
	fmt.Printf("The cat: %s\n", cat)

	cat.SetNameOfCopy("little pig")
	fmt.Printf("The cat: %s\n", cat)

	type Pet interface {
		SetName(name string)
		Name() string
		Category() string
		ScientificName() string
	}

	_, ok := interface{}(cat).(Pet)
	fmt.Printf("Cat implements interface Pet: %v\n", ok)
	_, ok = interface{}(&cat).(Pet)
	fmt.Printf("*Cat implements interface Pet: %v\n", ok)
}

思考题

  1. 我们可以在结构体类型中嵌入某个类型的指针类型吗?如果可以,有哪些注意事项?

我们可以在结构体中嵌入某个类型的指针类型, 它和普通指针类似,默认初始化为nil,因此在用之前需要人为初始化,否则可能引起错误

  1. 字面量struct{}代表了什么?又有什么用处?

空结构体不占用内存空间,但是具有结构体的一切属性,如可以拥有方法,可以写入channel。所以当我们需要使用结构体而又不需要具体属性时可以使用它。

12-06 23:19