结构体及其方法
结构体类型表示的是实实在在的数据结构。一个结构体类型可以包含若干个字段,每个字段通常都需要有确切的名字和类型。
结构体类型也可以不包含任何字段,这样并不是没有意义的,因为我们还可以为这些类型关联上一些方法,这里你可以把方法看做是函数的特殊版本。
方法和函数不同,它需要有名字,不能被当作值来看待,最重要的是,它必须隶属于某一个类型。方法所属的类型会通过其声明中的接收者(receiver)声明体现出来。
方法隶属的类型其实并不局限于结构体类型,但必须是某个自定义的数据类型,并且不能是任何接口类型。
方法的使用
接收者声明就是在关键字func和方法名称之间的那个圆括号包裹起来的内容,其中必须包含确切的名称和类型字面量。这个接收者的类型其实就是当前方法所属的那个类型,而接收者的名称,则用于在当前方法中引用它所属的类型的当前值。
举个例子:
// 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)
}
在Go语言中,我们可以通过为一个类型编写名为String的方法,来自定义该类型的字符串表示形式。这个String方法不需要任何参数声明,但需要有一个string类型的结果声明。所以在再用fmt包里的函数时,会打印出上面自定义的字符串表示形式,而无需显示的调用它的String方法。
我们可以把结构体类型中的一个字段看作是它的一个属性或者一项数据,再把隶属于它的一个方法看作是附加在其中数据之上的一个能力或者一项操作。将属性及其能力(或者说数据及其操作)封装在一起,是面向对象编程(object-orientedprogramming)的一个主要原则。
匿名字段
下面声明了一个结构体类型Animal,有两个字段,一个是string类型的scientificName。另一个字段声明中只有AnimalCategory,就是上面示例的那个结构体的名字:
type Animal struct {
scientificName string // 学名
AnimalCategory // 动物基本分类
}
字段声明AnimalCategory代表了Animal类型的一个嵌入字段。Go语言规范规定,如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。我们可以通过此类型变量的名称后跟“.”,再后跟嵌入字段类型的方式引用到该字段。也就是说,嵌入字段的类型既是类型也是名称。
强调一下,Go语言中没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之间的组合。
简单来说,面向对象编程中的继承,其实是通过牺牲一定的代码简洁性来换取可扩展性,而且这种可扩展性是通过侵入的方式来实现的。类型之间的组合采用的是非声明的方式,我们不需要显式地声明某个类型实现了某个接口,或者一个类型继承了另一个类型。
同时,类型组合也是非侵入式的,它不会破坏类型的封装或加重类型之间的耦合。我们要做的只是把类型当做字段嵌入进来,然后坐享其成地使用嵌入字段所拥有的一切。如果嵌入字段有哪里不合心意,我们还可以用“包装”或“屏蔽”的方式去调整和优化。
值方法和指针方法
方法的接收者类型必须是某个自定义的数据类型(不能是接口)。所谓的值方法,就是接收者类型是非指针的自定义数据类型的方法。之前的示例中的方法都是值方法。
下面的这个就是指针方法:
func (a *Animal) SetScientificName(name string) {
a.scientificName = name
}
方法的接受者类型是*Animal,是一个指针类型。这时Animal可以被叫做*Animal的基本类型。可以认为,指针类型的值就是指向某个基本类型值的指针。指针方法,就是接收者类型是上述指针类型的方法。
值方法和指针方法之间的不同点:
- 值方法的接收者是方法所属类型的一个副本。在方法内对副本的修改一般不会提现在原值上,除非这个类型本身是某个引用类型。而指针方法内对的修改是一定会提现在原值上的。
- 严格来讲,通过值只能调用到值方法,通过指针只能调用到指针方法。但是,Go会适时的进行自动的转义,使得通过值也能调用到它的指针方法。比如,
Animal.SetScientificName("Duck")
会自动转义为(&Animal).SetScientificName("Duck")
,即:先取指针值,然后再在改指针值上调用指针方法。
- 这条和接口相关,一个类型的方法集合中有哪些方法与它能实现哪些接口类型是息息相关的。如果一个基本类型和它的指针类型的方法集合是不同的,那么它们具体实现的接口类型的数量就也会有差异,除非这两个数量都是零。比如,一个指针类型实现了某某接口类型,但它的基本类型却不一定能够作为该接口的实现类型。
这个是验证上述差异的示例:
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) // false
_, ok = interface{}(&cat).(Pet)
fmt.Printf("*Cat implements interface Pet: %v\n", ok) // true
}
这里牵涉到了接口的知识点,所以这个例子和下面的内容,下一篇还会再讲一遍。
最后的2行输出的内容,说明cat没有实现Pet的接口,而&cat是实现了Pet的接口。
因为要实现Pet接口需要实现接下的那4个方法。而Cat类型没有实现SetName方法,所以cat没有实现Pet接口。代码中SetName方法是通过*Cat实现的,另外其他的3个方法都已经通过Cat实现了,通过*Cat也能调用(差异的第2条),所以只有指针方法实现了Pet接口的所有方法。