【GO高级编程】05.类的扩展与复用

1. 复合

Go 语言不支持传统面向对象中的"继承"(inheritance) ,但它通过 组合(composition)结构体嵌入(embedding) 实现代码复用,这种方式更灵活、更安全,也符合 Go 的设计哲学:"组合优于继承"。

1.1. 结构体嵌入(匿名字段)

Go 允许在一个结构体中匿名嵌入另一个结构体,从而自动获得被嵌入结构体的方法和字段。

复制代码
package utils

import (
	"testing"
	"fmt"
)
// 定义上层的结构体
type Car struct {
	Name string
}

func (car *Car) Run(){
	fmt.Printf("%s 开始运行了", car.Name)
}
/// -------------------

/// 定义实际的结构体
type XiaomiCar struct {
	Car
	Price float32
}

func (xiaomicar XiaomiCar) GetInto(){
	fmt.Printf("名称:%s\n价格:%.2f万\n", xiaomicar.Name, xiaomicar.Price)
}
///--------------------

// 还可以重写上层机构体的方法
// func (XiaomiCar *XiaomiCar) Run(){
// 	fmt.Printf("%s 开始运行了!!!", XiaomiCar.Name)
// }

func TestCar(t *testing.T){
	t.Log("=======汽车测试======")
	var xiaomicar XiaomiCar = XiaomiCar{
		Car: Car{Name:"小米SU7"},
		Price:19.9,
	}
	// 调用自己的方法
	xiaomicar.GetInto()
	// 调用内嵌的结构体方法
	xiaomicar.Run()
}

关键点:

  • XiaomiCar 并不是 Car 的子类,但通过嵌入,它"拥有"了 Car 的所有字段和方法。
  • 这叫 方法提升(method promotion)
  • 如果 XiaomiCar 定义了同名方法(如 Run),则优先调用自己的(类似"覆盖")

1.2. 多重组合(多"父类")

Go 支持嵌入多个结构体,轻松实现"多重继承"的效果(而不会出现菱形问题)。

复制代码
package utils

import (
	"testing"
	"fmt"
)


type Swimmer struct{}

func (s *Swimmer) Swim() {
	fmt.Println("Swimming...")
}

type Flyer struct{}

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


// Duck 既能游泳又能飞
type Duck struct {
	Swimmer // 嵌入
	Flyer   // 嵌入
	Name    string
}

func TestMult(t *testing.T){
	t.Log("=====多重组合=====")
	duck := Duck{Name:"黑天鹅鸭子"}
	duck.Fly()
	duck.Swim()
}

"菱形问题"(Diamond Problem)是多重继承(Multiple Inheritance)中一个经典且棘手的问题,尤其在像 C++ 这样的支持类多重继承的语言中经常出现。

当一个子类 同时继承两个父类 ,而这两个父类又共同继承自同一个祖父类时,就会形成一个"菱形"的继承结构:

A

/ \

B C

\ /

D

此时,如果 A 中有一个方法或字段,D 在调用它时,编译器无法确定应该使用哪一条路径继承下来的版本 ------是从 B 还是从 C?这就产生了歧义,称为 菱形问题

Go不会出现菱形是因为这是嵌入不是继承;另外如下这种写法会直接编译报错

复制代码
type A struct{}
func (a A) Hello() { fmt.Println("A") }

type B struct{ A }
type C struct{ A }
type D struct{ B; C } // 嵌入 B 和 C,它们都包含 A

func main() {
	d := D{}
	d.Hello() // ❌ 编译错误!ambiguous selector d.Hello
}

但是类的接口可以实现多继承

  • 接口只是"契约"

  • 实现者只要实现方法即可

  • 方法名相同但签名一致 → 合法

  • 签名不一致 → 编译错误

    type Flyer interface { Fly() }
    type Runner interface { Run() }
    type Super interface {
    Flyer
    Runner
    Fly() // 重复声明 Fly() 没问题
    }

总结:为什么说 Go "不能出现菱形继承"?

|-----------|-------------------------------------|
| 语言特性 | Go 的处理方式 |
| 没有类继承 | 只有组合(嵌入),不是"is-a",而是"has-a" |
| 嵌入冲突 | 编译时报错:"ambiguous selector",强制用户显式指定 |
| 接口合并 | 允许方法重叠,只要签名一致,无歧义 |
| 设计哲学 | 显式优于隐式,拒绝模糊行为 |

1.3. 接口 + 组合 = 真正的多态

  1. 定义接口A

  2. B实现接口A

  3. C嵌入B,从而拥有了B的方法,又因为B实现了A接口的方法,所以C也变相实现了接口,从而可以实现多态

    package utils

    import (
    "testing"
    "fmt"
    )
    // 1. 定义接口
    type Speaker interface {
    Speak()
    }

    // 定义实现接口的类
    type Animal2 struct{
    Name string
    }

    func (a *Animal2) Speak(){
    fmt.Println(a.Name,"speak....")
    }
    // 实际的类Dog2
    type Dog2 struct{
    Animal2
    }
    // 实际的类Cat2
    type Cat2 struct{
    Animal2
    }

    func (c *Cat2) Speak(){
    fmt.Println("Cat2", c.Name, "says meow!")
    }

    func SpeakTest(s Speaker){
    fmt.Println("--多态---")
    s.Speak()
    }

    func TestSpeak(t *testing.T){
    t.Log("=-====接口+组合=真正的多态=====")
    dog := &Dog2{Animal2{Name:"铁牛"}}
    cat := &Cat2{Animal2: Animal2{Name:"铁猫"}}
    // 打印结构体
    dog.Speak()
    cat.Speak()
    SpeakTest(dog)
    SpeakTest(cat)
    }

为什么 **SpeakTest(dog)**可以调用成功?

答案的核心就是因为Animal实现了接口方法,从而实现了接口,也因为Dog2嵌入了Animal也是隐式实现了接口的方法Speak() 所以也实现了接口,所以这里可以直接实现类型转换Speaker

1.3.1. 接口的隐式实现(Implicit Interface Implementation)

在 Go 中,一个类型只要实现了接口定义的所有方法,就自动"实现"了该接口不需要显式声明 (比如 Java 的 implements)。

定义了接口

复制代码
type Speaker interface {
    Speak()
}

*Dog2 类型是否实现了 Speak() 方法?

  • Dog2 嵌入了 Animal2
  • Animal2 有方法:func (a *Animal2) Speak()
  • 由于 方法提升(method promotion)*Dog2 自动拥有了 Speak() 方法

因此:

复制代码
var _ Speaker = (*Dog2)(nil) // 这行不会报错!说明 *Dog2 实现了 Speaker

所以 dog(类型为 *Dog2)可以作为 Speaker 接口传给 SpeakTest 函数。


1.3.2. 结构体嵌入带来方法提升(Method Promotion)

当这样定义:

复制代码
type Dog2 struct {
    Animal2  // 匿名嵌入
}

Go 会自动把 Animal2 的所有 字段和方法 "提升"到 Dog2 层级(前提是不冲突)。

也就是说:

  • dog.Name 等价于 dog.Animal2.Name
  • dog.Speak() 等价于 dog.Animal2.Speak()

而且这个 Speak() 方法的接收者虽然是 *Animal2,但 Go 允许通过 *Dog2 调用它,因为嵌入关系建立了"拥有"关系。

📌 注意:提升后的方法,其接收者类型仍然是原始类型(这里是 *Animal2),但调用语法上可以直接用外层类型。


1.3.3. 🔍 验证:*Dog2 是否真的实现了 Speaker

你可以加一行编译时检查:

复制代码
func TestSpeak(t *testing.T) {
    // 编译期验证:确保 *Dog2 实现了 Speaker
    var _ Speaker = (*Dog2)(nil)
    var _ Speaker = (*Cat2)(nil)

    // ...其余代码
}

如果没实现,这行会编译失败。而你的代码能运行,说明确实实现了。


1.3.4. 🧠 补充:值 vs 指针接收者的影响

注意你的方法是定义在 指针接收者 上的:

复制代码
func (a *Animal2) Speak()

这意味着:

  • *Animal2 实现了 Speaker
  • Animal2(值类型)没有 实现 Speaker

同样:

  • *Dog2 实现了 Speaker(因为可以调用 (*Animal2).Speak()
  • Dog2(值类型)没有 实现 Speaker

所以你必须传 指针

复制代码
dog := &Dog2{...}  // ✅ 正确:*Dog2 实现了 Speaker
SpeakTest(dog)     // OK

// 如果写成:
dog2 := Dog2{...}
SpeakTest(dog2)    // ❌ 编译错误!Dog2(值)没有 Speak() 方法(因为 Speak 是指针方法)

1.3.5. ✅ 总结:为什么 SpeakTest(dog) 能成功?

|---------------|--------------------------------------------------|
| 原因 | 说明 |
| 1. 方法提升 | Dog2嵌入 Animal2,自动获得 Speak()方法 |
| 2. 接口隐式实现 | *Dog2Speak()方法 → 自动满足 Speaker接口 |
| 3. 传的是指针 | dog := &Dog2{...}*Dog2,而 Speak()是指针方法,匹配 |

💡 这正是 Go "组合 + 接口" 实现多态的经典范式:无需继承,也能统一行为

相关推荐
Tony Bai3 小时前
Jepsen 报告震动 Go 社区:NATS JetStream 会丢失已确认写入
开发语言·后端·golang
bing.shao3 小时前
Golang 之 defer 延迟函数
开发语言·后端·golang
penngo3 小时前
Golang使用Fyne开发桌面应用
开发语言·后端·golang
ByNotD0g6 小时前
Golang Green Tea GC 原理初探
java·开发语言·golang
NaturalHarmonia6 小时前
【Go】sync package官方示例代码学习
开发语言·学习·golang
海上彼尚7 小时前
Go之路 - 7.go的函数
开发语言·后端·golang
weixin_449290017 小时前
Ubuntu 系统上安装和配置 Go 语言运行环境
linux·ubuntu·golang
古城小栈8 小时前
Go并发编程:RWMutex与Channel 紫禁之巅
golang
GokuCode8 小时前
【GO高级编程】02.GO接收者概述
开发语言·后端·golang