Go 零基础教程

Go 结构体嵌套

在很多传统的面向对象语言(如 Java 或 C++)中,“继承 (Inheritance)”是复用代码的主要手段。但在 Go 语言中,你会发现找不到 extends 关键字。

这是因为 Go 语言在设计上极其推崇“组合优于继承”的哲学。通过结构体嵌套 (Struct Embedding),你可以将简单的结构体像搭积木一样拼装成复杂的结构体。这种方式不仅避开了深度继承树带来的僵化和脆弱,还赋予了代码极高的灵活性和可维护性。

本章我们将彻底掌握这一强大的代码复用机制。

1. 什么是结构体嵌套?

结构体嵌套,也就是组合,是指将一个结构体作为另一个结构体的字段包含进去。这就好比是在说“汽车有一个 (has-a) 引擎”,而不是“汽车是一个 (is-a) 引擎”。

1.1 具名嵌套 (Named Embedding)

最基础的做法是给嵌套进来的结构体起一个明确的字段名。

package main

import "fmt"

type Address struct {
	Street  string
	City    string
}

type Person struct {
	Name    string
	Age     int
	Address Address // 具名嵌套:Address 是字段名,类型也是 Address
}

func main() {
	person := Person{
		Name: "John Doe",
		Age:  30,
		Address: Address{
			Street: "123 Main St",
			City:   "Anytown",
		},
	}
    // 必须通过完整的路径访问
	fmt.Println("城市:", person.Address.City) 
}

在这里,Person 包含了一个 Address。你需要顺藤摸瓜,使用完整的层级路径 person.Address.City 才能访问到内部的数据。

2. 匿名嵌套与提升魔法 (Anonymous Embedding & Promotion)

Go 语言提供了一种更具魔力的嵌套方式:匿名嵌套。你可以只写被嵌套的结构体类型,而不写字段名

当你这么做时,被嵌套结构体内部的所有字段,都会被“提升 (Promoted)”到外部结构体中!

2.1 字段提升示例

package main

import "fmt"

type Address struct {
	Street  string
	City    string
}

type Person struct {
	Name string
	Age  int
	Address // 匿名嵌套:只写类型名
}

func main() {
	person := Person{
		Name: "John Doe",
		Age:  30,
		Address: Address{
			Street: "123 Main St",
			City:   "Anytown",
		},
	}
	
	// ✨ 魔法发生:我们可以跳过 Address,直接访问 City 和 Street!
	fmt.Println("城市:", person.City)   // 提升字段
	fmt.Println("街道:", person.Street) // 提升字段
    
    // 当然,完整路径依然有效
    // fmt.Println(person.Address.City) 
}

2.2 具名嵌套 vs. 匿名嵌套 对比

特性具名嵌套 (Addr Address)匿名嵌套 (Address)
定义方式提供明确的字段名和类型。仅提供类型名。
访问路径必须使用完整层级 (如 p.Addr.City)。直接访问,享受提升特性 (如 p.City)。
适用场景包含多个同类型子结构体(如 HomeAddr, WorkAddr)。模拟“继承”行为,期望对外直接暴露子结构体的属性和能力。

3. 方法的提升 (Method Promotion)

匿名嵌套的魅力不仅仅在于字段,被嵌套结构体所拥有的方法,同样会被提升到外部结构体上!

这意味着,外部结构体“自动”获得了内部结构体的所有行为。

package main

import "fmt"

type Engine struct {
	Cylinders int
}

// Start 是 Engine 类型的方法
func (e Engine) Start() {
	fmt.Println("引擎启动,气缸数:", e.Cylinders)
}

type Car struct {
	Make  string
	Model string
	Engine // 匿名嵌套 Engine
}

func main() {
	car := Car{
		Make:  "Toyota",
		Model: "Camry",
		Engine: Engine{Cylinders: 4},
	}
	
	// Car 并没有定义 Start 方法,但因为它匿名嵌套了 Engine,所以它直接继承了这个能力!
	car.Start() 
}

4. 处理命名冲突 (Name Conflicts)

如果外层结构体和被嵌套的内层结构体拥有同名的字段或方法,会发生什么?
Go 的规则非常简单直白:外层永远胜出(就近原则)

外层的同名字段或方法会“遮蔽 (Shadow)”内层的字段或方法。
package main

import "fmt"

type Engine struct {
	Power int
}

type Car struct {
	Make  string
	Engine      // 匿名嵌套
	Power int   // ⚠️ 外层也有一个叫 Power 的字段!
}

func main() {
	car := Car{
		Make: "Toyota",
		Engine: Engine{Power: 150},
		Power: 200, 
	}
	
	// 默认访问的是外层的 Power (200)
	fmt.Println("汽车标称动力:", car.Power)         
	
	// 如果需要访问内层被遮蔽的 Power,必须写出完整路径
	fmt.Println("真实引擎动力:", car.Engine.Power) 
}

通过这种机制,你可以非常轻松地“重写 (Override)”被嵌套结构体的默认行为。

5. 多重嵌套与实战应用

Go 允许你将多个结构体同时匿名嵌套进一个结构体中。这让你能像挑选零件一样,把不同的功能模块组合到一个主体上。

例如,为业务服务动态增加日志能力:

package main

import "fmt"

// 日志模块
type Logger struct {
	Prefix string
}

func (l Logger) Log(message string) {
	fmt.Println(l.Prefix + ": " + message)
}

// 核心业务服务
type Service struct {
	Name string
	Logger // 接入日志模块能力
}

func (s Service) Run() {
    // 直接调用提升上来的 Log 方法
	s.Log("服务开始启动...") 
	fmt.Println("服务", s.Name, "正在全速运行")
	s.Log("服务运行结束。")
}

func main() {
	service := Service{
		Name: "AuthService",
		Logger: Logger{Prefix: "[权限校验]"},
	}
	service.Run()
}

在这个例子中,Service 根本不需要自己去写日志打印逻辑,直接“组合”一个 Logger 进来,就立刻拥有了打印带前缀日志的超能力。这就是组合的艺术。