18.4 嵌入与聚合

结构体中包含匿名(内嵌)字段叫嵌入或者内嵌;而如果结构体中字段包含了类型名,还有字段名,则是聚合。

聚合的在Java和C++都是常见的方式,而内嵌则是Go 的特有方式。

type Human struct {
	name string
}
type Person1 struct {           // 内嵌
	Human
}
type Person2 struct {           // 内嵌, 这种内嵌与上面内嵌有差异
	*Human
}
type Person3 struct{             // 聚合
	human Human
}

嵌入在结构体中广泛使用,在Go语言中如果只考虑结构体和接口的嵌入组合方式,一共有下面四种:

1. 在接口中嵌入接口:

这里指的是在接口中定义中嵌入接口类型,而不是接口的一个实例,相当于合并了两个接口类型定义的全部函数。下面只有同时实现了Writer和 Reader 的接口,才可以说是实现了Teacher接口,即可以作为Teacher的实例。Teacher接口嵌入了Writer和 Reader 两个接口,在Teacher接口中,Writer和 Reader是两个匿名(内嵌)字段。

type Writer interface{
   Write()
}
type Reader interface{
   Read()
} 
type Teacher interface{
  Reader
  Writer
}

2. 在接口中嵌入结构体:

这种方式在Go语言中是不合法的,不能通过编译。

type Human struct {
	name string
}
type Writer interface {
	Write()
}
type Reader interface {
	Read()
}
type Teacher interface {
	Reader
	Writer
	Human
}

存在语法错误,并不具有实际的含义,编译报错: interface contains embedded non-interface Base

Interface 不能嵌入非interface的类型。

3. 在结构体中内嵌接口:

初始化的时候,内嵌接口要用一个实现此接口的结构体赋值;或者定义一个新结构体,可以把新结构体作为receiver,实现接口的方法就实现了接口(先记住这句话,后面在讲述方法时会解释),这个新结构体可作为初始化时实现了内嵌接口的结构体来赋值。

package main
import (
	"fmt"
)
type Writer interface {
	Write()
}
type Author struct {
	name string
	Writer
}
// 定义新结构体,重点是实现接口方法Write()
type Other struct {
	i int
}
func (a Author) Write() {
	fmt.Println(a.name, "  Write.")
}
// 新结构体Other实现接口方法Write(),也就可以初始化时赋值给Writer 接口
func (o Other) Write() {
	fmt.Println(" Other Write.")
}
func main() {
	//  方法一:Other{99}作为Writer 接口赋值
	Ao := Author{"Other", Other{99}}
	Ao.Write()
	// 方法二:简易做法,对接口使用零值,可以完成初始化
	Au := Author{name: "Hawking"}
	Au.Write()
}
程序输出:
Other   Write.
Hawking   Write.

4. 在结构体中嵌入结构体:

结构体嵌入结构体很好理解,但不能嵌入自身值类型,可以嵌入自身的指针类型即递归嵌套。

在初始化时,内嵌结构体也进行赋值;外层结构自动获得内嵌结构体所有定义的字段和实现的方法。

下面代码完整演示了结构体中嵌入结构体,初始化以及字段的选择调用:

package main
import (
	"fmt"
)
type Human struct {
	name   string // 姓名
	Gender string // 性别
	Age    int    // 年龄
	string        // 匿名字段
}
type Student struct {
	Human     // 匿名字段
	Room  int // 教室
	int       // 匿名字段
}
func main() {
	//使用new方式
	stu := new(Student)
	stu.Room = 102
	stu.Human.name = "Titan"
	stu.Gender = "男"
	stu.Human.Age = 14
	stu.Human.string = "Student"
	fmt.Println("stu is:", stu)
	fmt.Printf("Student.Room is: %d\n", stu.Room)
	fmt.Printf("Student.int is: %d\n", stu.int) // 初始化时已自动给予零值:0
	fmt.Printf("Student.Human.name is: %s\n", stu.name) //  (*stu).name
	fmt.Printf("Student.Human.Gender is: %s\n", stu.Gender)
	fmt.Printf("Student.Human.Age is: %d\n", stu.Age)
	fmt.Printf("Student.Human.string is: %s\n", stu.string)
	// 使用结构体字面量赋值
	stud := Student{Room: 102, Human: Human{"Hawking", "男", 14, "Monitor"}}
	fmt.Println("stud is:", stud)
	fmt.Printf("Student.Room is: %d\n", stud.Room)
	fmt.Printf("Student.int is: %d\n", stud.int) // 初始化时已自动给予零值:0
	fmt.Printf("Student.Human.name is: %s\n", stud.Human.name)
	fmt.Printf("Student.Human.Gender is: %s\n", stud.Human.Gender)
	fmt.Printf("Student.Human.Age is: %d\n", stud.Human.Age)
	fmt.Printf("Student.Human.string is: %s\n", stud.Human.string)
}
程序输出:
stu is: &{ {Titan 男 14 Student} 102 0}
Student.Room is: 102
Student.int is: 0
Student.Human.name is: Titan
Student.Human.Gender is: 男
Student.Human.Age is: 14
Student.Human.string is: Student
stud is: { {Hawking 男 14 Monitor} 102 0}
Student.Room is: 102
Student.int is: 0
Student.Human.name is: Hawking
Student.Human.Gender is: 男
Student.Human.Age is: 14
Student.Human.string is: Monitor

内嵌结构体的字段,可以逐层选择来使用,如stu.Human.name。如果外层结构体中没有同名的name字段,也可以直接选择使用,如stu.name。

通过对结构体使用 new(T)struct{filed:value} 两种方式来声明初始化,分别可以得到*T指针变量,和T值变量。

从上面程序输出结果中 stu is: &{ {Titan 男 14 Student} 102 0} 可以得知,stu 是指针变量。但是程序在调用此结构体变量的字段时并没有使用到指针,这是因为这里的 stu.name 相当于(*stu).name,这是一个语法糖,一般都使用stu.name方式来调用,但要知道有这个语法糖存在。