10-结构体类型

一个结构体类型包含若干个字段、每个字段都需要有确切的名字和类型。结构体类型也可以不包含任何字段,这并不是没有意义,可以给结构体类型关联上一些方法,把方法看做是函数的特殊版本。

函数是独立的程序实体,可以声明有名字的函数或者匿名函数,还可以把函数当做值传递。可以把具有相同签名的函数抽象成独立的函数类型,作为一组输入、输出的代表

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

接收者声明就是在关键字func和方法名之间的圆括号包裹起来的内容,其中必须包含确切的名称和类型字面量。

例子:

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

// 隶属于AnimalCategory类型的方法 String(),方法的接收者是ac
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)
}
// 通过方法的接收者ac,可以在其中引用到当前值的任何一个字段,
// 或者调用当前值的任何一个方法(包括string方法自己)

func main(){
    category := AnimalCategory{species: "cat"}
    fmt.Printf("The animal category: %s\n",category)
    // 无需显示调用String方法,即可打印出该类型的字符串表示形式
}

在Go语言中,通过为一个类型编写名为String的方法,来自定义该类型的字符串表示形式。这个String方法不需要任何参数声明,但是需要有一个string类型的结果输出。

方法隶属的类型不局限于结构体类型,但必须是某个自定义的数据类型,并且不能是任何接口类型。

  • 一个数据类型关联的所有方法,共同组成了该类型的方法集合
  • 同一个方法集合中的方法不能出现重名,且不能与该类型中任何字段的名称重复

0.1. 嵌入字段

面向对象编程的主要原则:将数据及其操作封装在一起。在Go语言中,把结构体类型中的一个字段看作是一项数据,把隶属它的方法看作是附加在其中数据之上的操作。

例子:

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

// 在结构体类型的某个字段声明中只有一个类型名则将原类型嵌入到新的类型中

Go语言规范规定,如果一个字段的声明中只有字段的类型名,没有字段的名称,那么就是一个嵌入字段(也称为匿名字段)。通过此类型变量的名称后跟着“.”,在跟着嵌入字段类型的方式引用到该字段,即嵌入字段的类型既是类型也会名称。例如下面的a.AnimalCategory

func (a Animal) Category() string {
    return a.AnimalCategory.String()
}
// Category方法的接收者类型是Animal,接收者名称a,
// 通过a.AnimalCategory选择到a的嵌入字段
  • 选择表达式:在某个代表变量的标识符的右边加上“.”,在加上字段名或方法名,表示选择了某个字段或方法

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

animal := Animal{
    scientificName: "American Shorthair",
    AnimalCategory: category,
}
fmt.Printf("The animal: %s\n", animal)
// 此时会直接调用AnimalCategory的String方法

如果Animal也编写自己的String方法,那么嵌入字段的String会被屏蔽。只要名称相同,无论方法的签名是否一致,嵌入字段的方法都会被屏蔽

因为嵌入字段的字段和方法都可以“嫁接”到被嵌入类型上,所以即使在两个同名的成员一个是字段,另一个是方法的情况下,屏蔽现象依然存在。通过链式选择表达式选择被屏蔽的嵌入字段的字段或方法。

0.2. 多层嵌入

例子:

type Cat struct {
    name string
    Animal  // 嵌入字段本身也是嵌入字段
}

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

屏蔽现象会以嵌入的层级为依据,嵌入的层级越深的字段或方法越可能被屏蔽。如,调用Cat的String方法:

状态CatAnimalAnimalCategory
1存在,调用屏蔽屏蔽
2不存在调用屏蔽
3不存在不存在调用

根据是否存在String方法,来判断嵌入字段的String方法是否会被调用会屏蔽。

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

0.3. 类型组合

Go语言中不存在继承的概念,它通过嵌入字段的方式实现了类型之间的组合。面向对象编程中的继承,通过牺牲一定的代码简洁性来换取可扩展性,这种可扩展性是通过侵入的方式来实现的

类型之间的组合采用的是非声明的方式,是非侵入式的,它不会破坏类型的封装或加重类型之间的耦合。我们只是把类型当做字段嵌入进来,然后坐享其成地使用嵌入字段所拥有的一切。同时可以通过“包装”或“屏蔽”的方式来调整或优化嵌入字段。

类型组合非常灵活的通过嵌入字段把一个类型的属性和方法“嫁接”给另一个类型,被嵌入类型自然的实现了嵌入字段所实现的接口。组合比继承更加简洁和清晰,不会有多重继承那样复杂的层次结构和可观的管理成本

接口类型之间也可以组合,以此来扩展接口定义的型号或者标记接口的特征。

0.4. 值方法与指针方法

方法的接收者类型必须是某个自定义的数据类型,不能是接口类型或者接口的指针类型。

  • 值方法:接收者类型是非指针的自定义数据类型的方法
  • 指针方法:接收者类型是指针类型的方法

上面例子中的方法都是值方法。

指针方法,如下例子:

func (cat *Cat) SetName(name string) {
    cat.name = name
}
// SetName方法的接收者类型是*Cat所以是一个指针方法
  • 取值表达式:*放在一个指针值的左边,来获取该指针指向的基本类型值
  • 取地址表达式:&放在一个可寻址的基本类型值的左边,来获取该基本类型的指针值

值方法与指针方法的区别:

差别值方法指针方法
方法接收者该方法所属的那个类型值的一个副本,值方法内对该副本的修改不会体现在原值上,除非这个类型本身是某个引用类型的别名类型该方法所属的那个基本类型值的指针的一个副本,指针方法内对该副本的修改会直接体现在原值上
方法集合自定义数据类型的方法集合中仅包含所有值方法该类型的指针类型的方法集合包括所有的值方法和指针方法

方法集合的解释:严格来说,基本类型的值上之只能调用它的值方法,但,Go语言会适时地为我们进行自动地转译,使得我们在这样的值上也能调用它的指针方法。例如,cat.SetName("monster")会自动转译为(&cat).SetName("monster"),即先取cat的指针值,然后在指针值上调用指针方法SetName。

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

上次修改: 25 November 2019