概念

  • 结构体:类似于产品说明书。它详细描述了一个东西是什么,有什么属性(比如属性、尺寸)关注的是数据

  • 接口:类似于技能证书或岗位要求。它规定了一个东西必须能做什么,它不关心这个东西到底是什么,是人是狗,只要它满足这个要求就行。它关注的是行为。

接口-定义“行为规范”

什么是接口

在Go中,接口是一组方法签名的集合。它定义了一个对象需要具备哪些行为,但没有具体实现这些行为

核心思想:只关系“能做什么”,不关心“是什么”

定义一个接口

使用type和interface关键字

type 接口名 interface {
  方法名1(参数列表1) 返回值列表1
  方法名2(参数列表2) 返回值列表2
  //...
}

定义一个“会说话”的接口

// 定义一个名为 Sayer 的接口
// 它规定:任何实现了 Sayer 接口的类型,都必须有一个 say() 方法,该方法返回一个字符串
type Sayer interface {
    say() string
}

Sayer 接口就像一份合同,上面写着:“想要成为 Sayer,就必须实现 say() 方法”。但它自己不提供 say() 方法的具体代码。

让Dog和Cat实现Sayer接口

package main

import "fmt"

// 1.定义接口Sayer
type Sayer interface {
    say() string
}

c

// 3.为Dog结构体实现say()方法
// 注意:这个方法的接收者是 *Dog类型
func (d *Dog) say() string {
    return "汪汪汪"
}

// 定义结构体 Cat
type Cat struct {
}

// 为Cat结构体实现say()方法
func (c *Cat) say() string {
    return "喵喵喵"
}

func mian() {
    // 声明一个Sayer接口类型变量
    var sayer Sayer

    // 创建一个Dog实例
    myDog := &Dog{}

    sayer = myDog
    fmt.Println("Cat says:%s\n", sayer.say())
}
  • 定义了Sayer接口

  • 定义了Dog和Cat两个结构体

  • 分别为Dog和Cat添加了say()方法。一旦添加成功,Dog和Cat就自动变成Sayer的类型

  • 在main函数中,可以定义一个Sayer类型的变量sayer

  • 无论是 Dog 的实例还是 Cat 的实例,都可以赋值给 sayer。这就是多态的体现!同一个 sayer 变量,在不同时刻代表了不同的具体类型(Dog 或 Cat),但调用 say() 方法时,会执行各自的具体实现。

为什么要用接口

  • 解耦:将定义和实现分开。调用方只需要关心接口,而不需要关心具体是哪个结构体实现的。

  • 通用性和灵活性:可以编写接受接口类型作为参数的函数,这样这个函数就能处理任何实现了该接口的类型,大大提高了代码的复用性。

一个通用的“叫”函数

// 这个函数不关心传进来的是狗还是猫,只要是 Sayer 就行
func makeItTalk(s Sayer) {
    fmt.Println("它开始说话了:", s.say())
}

func main() {
    makeItTalk(&Dog{}) // 输出: 它开始说话了: 汪汪汪~
    makeItTalk(&Cat{}) // 输出: 它开始说话了: 喵喵喵~
}

结构体-封装“数据”

什么是结构体

结构体是Go中一种聚合数据类型,它把一系列具有相同/不同类型的变量(称为字段)组合在一起,形成了一个自定义类型。它就像其他语言中的“类”,但没有继承。

核心思想:将相关的数据打包成一个整体。

如果定义一个结构体

使用type和struct关键字

type 结构体名 struct {
    字段名1 字段类型1
    字段名2 字段类型2
    // ...
}

定义一个“人”的结构体

// 定义一个名为 Person 的结构体
type Person struct {
    Name string // 姓名
    Age  int    // 年龄
    City string // 城市
}

如何使用结构体

创建和操作结构体实例

func main() {
    // 方式1:声明一个变量,然后逐个赋值
    var p1 Person
    p1.Name = "张三"
    p1.Age = 30
    p1.City = "北京"
    fmt.Printf("%+v\n", p1) // 输出: {Name:张三 Age:30 City:北京}

    // 方式2:使用键值对方式初始化(推荐,更清晰)
    p2 := Person{
        Name: "李四",
        Age:  25,
        City: "上海",
    }
    fmt.Printf("姓名: %s, 年龄: %d\n", p2.Name, p2.Age) // 输出: 姓名: 李四, 年龄: 25

    // 方式3:按顺序初始化(不推荐,容易出错)
    p3 := Person{"王五", 28, "广州"}
    fmt.Println(p3) // 输出: {王五 28 广州}
}

结构体方法

结构体只有数据是不够的,我们还需要定义操作这些数据的行为,这就是方法。

方法是一种特殊的函数,它多了一个接收者。这个接收者就是某个类型的实例。

func (接收者变量 接收者类型) 方法名(参数列表) 返回值列表 {
    // 方法体
}

为Person结构体添加方法

type Person struct {
    Name string
    Age  int
}

// 为 Person 结构体添加一个 "自我介绍" 的方法
// (p Person) 就是接收者,表示这个方法是属于 Person 类型的
func (p Person) Introduce() {
    fmt.Printf("大家好,我叫%s,今年%d岁。\n", p.Name, p.Age)
}

// 添加一个 "过生日" 的方法,这个方法会修改结构体的值
// 这里使用指针作为接收者,因为我们需要修改原始数据
func (p *Person) HaveBirthday() {
    fmt.Printf("%s 正在过生日...\n", p.Name)
    p.Age++ // 年龄加1
}

func main() {
    p := Person{Name: "小明", Age: 18}

    // 调用方法
    p.Introduce() // 输出: 大家好,我叫小明,今年18岁。

    p.HaveBirthday() // 输出: 小明 正在过生日...
    p.Introduce()    // 输出: 大家好,我叫小明,今年19岁。
}
  • 如果方法不需要修改接收者的值,通常使用值接收者 (p Person)。

  • 如果方法需要修改接收者的值,或者接收者本身很大(复制成本高),必须使用指针接收者 (p *Person)。

接口与结构体的完美结合

场景:我们要计算不同图形的面积和周长

  1. 定义接口:定义一个 Geometry 接口,规定所有图形都必须有 area() 和 perimeter() 方法。

  2. 定义结构体:定义 Rectangle (矩形) 和 Circle (圆形) 结构体,它们拥有各自的属性(长宽、半径)。

  3. 实现接口:为 Rectangle 和 Circle 分别实现 area() 和 perimeter() 方法。

  4. 使用接口:编写一个函数,接受 Geometry 接口类型作为参数,来统一处理所有图形。

package main

import (
    "fmt"
    "math"
)

// 1.定义接口Geometry
type Geometry interface {
    area() float64
    perimeter() float64
}

// 2. 定义结构体Rectangle
type Rectangle struct {
    width, height float64
}

// 3.为Rectangle实现接口方法
func (r Rectangle) area() float64 {
    return r.width * r.height
}

func (r Rectangle) perimeter() float64 {
    return 2 * (r.width + r.height)
}

// 4. 定义结构体Circle
type Circle struct {
    radius float64
}

// 5.为Circle实现接口方法
func (c Circle) area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

// 6.编写通用函数,接受Geometry接口类型参数
func measure(g Geometry) {
    fmt.Println("图形信息:")
    fmt.Printf("- 面积: %.2f\n", g.area())
    fmt.Printf("- 周长: %.2f\n", g.perimeter())
    fmt.Println("--------------------")
}

func main() {
    // 创建一个矩形实例
    r := Rectangle{width: 10, height: 5}
    // 创建一个圆形实例
    c := Circle{radius: 7}

    // 调用通用函数measure
    measure(r)
    measure(c)
}

image.png

总结

  • 结构体:是数据的载体,它定义了一个事物“是什么”,由一系列字段组成。

  • 接口:是行为的规范,它定义了一个事物“能做什么”,由一系列方法签名组成。

  • 实现:Go中,一个类型只要实现了接口中的所有方法,就自动实现了该接口,无需显式声明。

  • 结合使用:结构体提供具体的数据和方法实现,接口则提供一种抽象的视角,让我们能够编写更通用、更灵活、更解耦的代码。

嵌套

结构体嵌套

结构体嵌套是指在一个结构体中,定义另一个结构体类型的字段。这有两种形式:有名嵌套和匿名嵌套。匿名嵌套是Go实现组合的关键。

有名嵌套(普通嵌套)

就像定义一个普通字段一样,只是字段的类型是另一个结构体。

package main

import "fmt"

// 地址结构体
type Address struct {
    Province string
    City     string
}

// 人的结构体,包含一个名为 HomeAddr 的 Address 类型字段
type Person struct {
    Name    string
    Age     int
    HomeAddr Address // 有名嵌套
}

func main() {
    p := Person{
        Name: "张三",
        Age:  30,
        HomeAddr: Address{ // 初始化时需要指明字段名
            Province: "北京",
            City:     "北京市",
        },
    }

    // 访问时需要通过字段名逐级访问
    fmt.Println(p.Name)                 // 输出: 张三
    fmt.Println(p.HomeAddr.City)        // 输出: 北京市
}

匿名嵌套

package main

import "fmt"

// 基础动物结构体
type Animal struct {
    Name string
}

func (a *Animal) Eat() {
    fmt.Printf("%s is eating.\n", a.Name)
}

// 狗结构体,匿名嵌套了 Animal
type Dog struct {
    Animal // 匿名嵌套,没有字段名
    Breed  string // 狗自己的品种
}

func (d *Dog) Bark() {
    fmt.Printf("%s is barking: Woof!\n", d.Name)
}

func main() {
    // 初始化时可以直接初始化嵌套的结构体
    myDog := Dog{
        Animal: Animal{
            Name: "旺财",
        },
        Breed: "哈士奇",
    }

    // *** 关键点:提升 ***
    // Animal 的字段和方法被“提升”到了 Dog
    // 我们可以直接访问 Name 和 Eat(),就像它们是 Dog 自己的一样
    fmt.Println(myDog.Name)   // 输出: 旺财 (直接访问 Animal.Name)
    myDog.Eat()               // 输出: 旺财 is eating. (直接调用 Animal.Eat())

    // Dog 自己的方法当然也可以调用
    myDog.Bark()              // 输出: 旺财 is barking: Woof!
}

当一个结构体匿名嵌套了另一个结构体,内部结构体的所有字段和方法都会被“提升”到外部结构体,可以直接通过外部结构体的实例访问,就好像这些字段和方法是外部结构体自己的一样。

如果外部和内部结构体有同名字段或方法,Go的访问规则是:

  1. 编译器会优先访问外部结构体定义的成员。

  2. 如果想访问被“遮蔽”的内部成员,可以通过完整的路径来访问,例如 myDog.Animal.Name。

接口嵌套

接口嵌套非常直观,就是将多个接口组合成一个更大的接口。这在Go标准库中非常常见。

package main

import "fmt"

// 定义一个 "读" 接口
type Reader interface {
    Read()
}

// 定义一个 "写" 接口
type Writer interface {
    Write()
}

// 嵌套 Reader 和 Writer,组合成一个更强大的 "读写" 接口
type ReadWriter interface {
    Reader // 嵌套 Reader 接口
    Writer // 嵌套 Writer 接口
}

// 定义一个文件结构体
type File struct {
    Name string
}

// File 实现 Read() 方法
func (f *File) Read() {
    fmt.Printf("Reading from file: %s\n", f.Name)
}

// File 实现 Write() 方法
func (f *File) Write() {
    fmt.Printf("Writing to file: %s\n", f.Name)
}

func main() {
    myFile := &File{Name: "test.txt"}

    // 因为 File 同时实现了 Read() 和 Write() 方法
    // 所以它自动实现了 Reader, Writer, 以及 ReadWriter 接口
    var rw ReadWriter = myFile

    rw.Read()  // 输出: Reading from file: test.txt
    rw.Write() // 输出: Writing to file: test.txt
}

如何永组合实现“继承”的效果

通过结构体的匿名嵌套(组合)。

package main

import "fmt"

// "父类"
type Animal struct {
    Name string
}

// "父类"的方法
func (a *Animal) Sleep() {
    fmt.Printf("%s is sleeping.\n", a.Name)
}

// “子类”
type Dog struct {
    Animal // 通过匿名嵌套继承“Animal”
}

// "子类"的新方法
func (d *Dog) Bark() {
    fmt.Printf("%s is barking.\n", d.Name)
}

// "子类"重写"父类"的方法
func (d *Dog) Sleep() {
    fmt.Printf("%s is sleeping in the sun.\n", d.Name)
}

func main() {
    myDog := Dog{Animal: Animal{Name: "Buddy"}}

    // 1. 代码复用:Dog 自动获得了 Animal 的 Name 字段
    fmt.Println(myDog.Name) // 输出: Buddy

    // 2. 调用“继承”来的方法
    myDog.Sleep() // 注意:这里调用的是 Dog 自己重写的方法

    // 3. 调用子类自己的方法
    myDog.Bark() // 输出: Buddy is barking!

    // 4. 方法重写
    myDog.Sleep() // 输出: Buddy is sleeping in a dog house.
}

在这个例子中,Dog 通过组合 Animal 实现了:

  • 代码复用:Dog 不用再定义 Name 字段,直接复用了 Animal 的。

  • 方法提升:Dog 可以直接调用 Animal 的方法(如果它没有重写的话)。

  • 方法重写:Dog 可以定义一个与 Animal 同名的方法,从而“覆盖”掉“父类”的实现。

  • 多态:一个 Dog 实例可以被赋值给一个 Animal 类型的变量(通常用指针)。

var a Animal = &Dog{Animal: Animal{Name: "Buddy"}}
a.Sleep() // 输出: Buddy is sleeping in a dog house.

Go的设计哲学:“组合优于继承”。