概念
结构体:类似于产品说明书。它详细描述了一个东西是什么,有什么属性(比如属性、尺寸)关注的是数据
接口:类似于技能证书或岗位要求。它规定了一个东西必须能做什么,它不关心这个东西到底是什么,是人是狗,只要它满足这个要求就行。它关注的是行为。
接口-定义“行为规范”
什么是接口
在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)。
接口与结构体的完美结合
场景:我们要计算不同图形的面积和周长
定义接口:定义一个
Geometry接口,规定所有图形都必须有area()和perimeter()方法。定义结构体:定义
Rectangle(矩形) 和Circle(圆形) 结构体,它们拥有各自的属性(长宽、半径)。实现接口:为
Rectangle和Circle分别实现area()和perimeter()方法。使用接口:编写一个函数,接受
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)
}

总结
结构体:是数据的载体,它定义了一个事物“是什么”,由一系列字段组成。
接口:是行为的规范,它定义了一个事物“能做什么”,由一系列方法签名组成。
实现: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的访问规则是:
编译器会优先访问外部结构体定义的成员。
如果想访问被“遮蔽”的内部成员,可以通过完整的路径来访问,例如
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的设计哲学:“组合优于继承”。