结构体类型表示的是实实在在的数据结构。一个结构体类型可以包含若干个字段,每个字段通常都需要有确切的名字和类型。
结构体类型也可以不包含任何字段,这样并不是没有意义的,因为我们还可以为这些类型关联上一些方法,这里你可以把方法看做是函数的特殊版本。
方法和函数不同,它需要有名字,不能被当作值来看待,最重要的是,它必须隶属于某一个类型。方法所属的类型会通过其声明中的接收者(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的基本类型。可以认为,指针类型的值就是指向某个基本类型值的指针。指针方法,就是接收者类型是上述指针类型的方法。
值方法和指针方法之间的不同点:
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接口的所有方法。
创新互联www.cdcxhl.cn,专业提供香港、美国云服务器,动态BGP最优骨干路由自动选择,持续稳定高效的网络助力业务部署。公司持有工信部办法的idc、isp许可证, 机房独有T级流量清洗系统配攻击溯源,准确进行流量调度,确保服务器高可用性。佳节活动现已开启,新人活动云服务器买多久送多久。