结构体内嵌类似OOP中的继承。

1. 类型内嵌

结构体允许其成员字段在声明时没有字段名,而只有类型。这种形式的字段称为匿名字段或类型内嵌。

type Data struct {
    int            // 定义结构体的匿名字段
    float32
    bool
}

ins := &Data{
    int: 10,       // 为Data实例中的字段赋初值
    float32: 3.14,
    bool: true,
}

结构体要求成员变量字段名称必须唯一,因此一个结构体中同类型的匿名字段只能有一个。


2. 声明结构体内嵌

如果匿名字段的类型为结构体,那么结构体实例化后,我们可以直接访问匿名结构体里的所有成员变量,此方式称为结构体内嵌。

  • 传统写法
package main

import "fmt"

// 定义基础颜色
type BasicColor struct {
    R, G, B float32
}

// 定义完整颜色
type Color struct {
    Basic BasicColor    // 基础颜色结构体
    Alpha float32       // 透明度
}

func main() {
    var c Color         // 结构体实例化
    
    c.Basic.R = 1       // 访问结构体实例的结构体类型成员变量并赋值
    c.Basic.G = 1
    c.Basic.B = 0
    
    c.Alpha = 1
    
    fmt.Printf("%+v", c)
}

// {Basic:{R:1 G:1 B:0} Alpha:1}
  • 结构体内嵌写法
package main

import "fmt"

// 定义基础颜色
type BasicColor struct {
    R, G, B float32
}

// 定义完整颜色
type Color struct {
    BasicColor    // 没有成员变量名只有类型,叫做结构体内嵌
    Alpha float32 // 透明度
}

func main() {
    var c Color   // 结构体实例化
    
    c.R = 1       // 直接访问实例c的匿名结构体里所有成员变量
    c.G = 1
    c.B = 0
    
    c.Alpha = 1
    
    fmt.Printf("%+v", c)
}

// {BasicColor:{R:1 G:1 B:0} Alpha:1}

3. 结构体内嵌特性

  • 结构体内嵌可以直接访问其成员变量

可以通过实例名直接访问嵌入结构体的成员变量。如果结构体有多层嵌入结构,通过实例访问任何一级嵌入结构成员变量时,只需给出字段名即可。如ins.a.b.c可以简写成ins.c。

  • 结构体内嵌的类型名是它的成员变量字段名

结构体内嵌仍然可以使用传统方式进行一层层访问,结构体内嵌的类型名是它的成员变量字段名。

var c Color

c.BasicColor.R = 1
c.BasicColor.G = 1
c.BasicColor.B = 0

一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值。


4. 使用“组合”描述对象

在OOP中,实现对象关系需要使用“继承”特性,如人类不能飞行,鸟类可以飞行。人类和鸟类都可以从行走类继承,但只有鸟类从飞行类继承。OOP原则建议对象最好不要使用多重继承,鸟类同时继承行走类和飞行类,这显然是有问题的。在OOP中要正确实现对象多重继承,需要技巧。

go语言的结构体内嵌是一种组合,类似于OOP中的继承,可以快速构建对象的不同特性。

package main

import "fmt"

// 天上飞的 
type Flying struct {}

func (f *Flying) Fly() {
    fmt.Println("can fly")
}

// 地上走的
type Walking struct {}

// 给Walking结构体添加Walk方法
func (w *Walking) Walk() {
    fmt.Println("can walk")
}


// 结构体内嵌
// 人类
type Human struct {
    Walking
}

// 鸟类
type Bird struct {
    Walking
    Flying
}

func main() {
    b := new(Bird)         // 结构体实例化
    fmt.Println("Bird: ")
    b.Fly()                // 实例调用方法
    b.Walk()
    
    h := new(Human)
    fmt.Println("Human: ")
    h.Walk()
}


/*
Bird: 
can fly
can walk
Human: 
can walk
*/

使用go语言的结构体内嵌组合,可以自由地对实例进行实现增、删、改各种操作。


5. 初始化结构体内嵌

结构体内嵌初始化时,把结构体内嵌的类型作为成员变量字段名,像普通结构体一样初始化。

package main

import "fmt"

// 车轮,基础构件
type Wheel struct {
    Size int
}

// 引擎,基础构件
type Engine struct {
    Power int 
    Type string
}

// 车,整体结构,结构体内嵌
type Car struct {
    Wheel 
    Engine
}

func main() {
    c := Car {
        Wheel: Wheel{
            Size: 18,
        },
        
        Engine: Engine{
            Power: 143,
            Type: "1.4T",
        },
    }
    fmt.Printf("%+v\n", c)
}

//{Wheel:{Size:18} Engine:{Power:143 Type:1.4T}}

6. 初始化匿名结构体内嵌

在初始化匿名内嵌结构体时,我们需要再次声明匿名结构体的结构,才能初始化赋值。使用“键值对”格式,初始化填充数据。

package main

import "fmt"

// 车轮,基础构件
type Wheel struct {
    Size int
}

// 车,整体结构
type Car struct {
    Wheel 
    Engine struct {    // 匿名内嵌结构体,匿名内嵌结构体不能被外部引用
        Power int 
        Type string
    }
}

func main() {
    c := Car {
        Wheel: Wheel{
            Size: 18,
        },
        
        Engine: struct { // 内嵌结构体类型名作为成员变量字段名
            Power int 
            Type  string
        }{
            Power: 143,
            Type: "1.4T",
        },
    }
    fmt.Printf("%+v\n", c)
}

注:声明内嵌结构体时,多个成员之间换行不用",";在初始化赋值时,结构体成员变量之间要有",",这点需要注意。


7. 成员名字冲突

内嵌结构体内部可能有相同的成员名,成员重名会导致编译器无法选择是哪个内嵌结构体的成员变量字段,引起歧义和错误。

package main

import "fmt"

type A struct {
    a int
}

type B struct {
    a int
}

type C struct {
    A
    B
}

func main() {
    c := &C{}  // 内嵌结构体实例化
    c.A.a = 1  // 成员变量类型已经定义,所以不用:=类型推断赋值,而用=
    fmt.Println(c)
    
    // c.a = 1  
    // fmt.Println(c)
}

// &{{1} {0}}