GO学习笔记 | 第三章节 GO语言基础 | 接口&&结构体&&方法接收器&&组合&&泛型
GO学习笔记 | 第三章节 GO语言基础 | 接口&&结构体&&方法接收器&&组合&&泛型
核心内容:接口、结构体、方法接收器、组合、衍生类型与类型别名、泛型
前置知识:方法声明、函数式编程、控制结构(if/for/switch)、内置类型
一、课前回顾:defer 与闭包
1.1 for 循环中 defer 的经典问题
1 | for i := 0; i < 10; i++ { |
原因:i 的地址始终是同一个,defer 延迟到函数返回前才执行,此时 i 已经是 10 了。
验证:打印地址确认
1 | for i := 0; i < 10; i++ { |
两种解决方案:
1 | // 方案1:传参(值传递,推荐) |
二、接口(Interface)

2.1 什么是接口
接口是一组行为的抽象——只规定「能做什么」,不关心「怎么做的」。
1 | type List interface { |
类比理解:
- 产品经理说:「我要一个能在特定位置增删改查的数据结构」→ 这就是接口
- 你可以用数组实现、链表实现、跳表实现…… → 这些都是具体实现
- 老板要结果,不关心是你做的还是同事做的 → 面向接口编程
2.2 接口特点
- 只能包含方法,不能包含字段
- 方法签名不需要
func关键字 - 方法名首字母大写 = 包外可访问,小写 = 包内私有
2.3 接口设计原则
当你怀疑要不要定义接口的时候,加上接口!
- 声明时用接口,而不是具体实现
- 系统的核心应该面向接口编程
- 后续讲依赖注入时会深入讲解
三、结构体(Struct)
3.1 结构体定义

1 | type User struct { |
访问控制:大写开头 = 包外可访问,小写开头 = 包内私有
3.2 结构体初始化

方式1:直接初始化(拿到结构体)
1 | u1 := User{} // 零值初始化 |
方式2:取地址(拿到指针)
1 | up1 := &User{ |

打印结构体
1 | u := User{Name: "Tom", Age: 18} |
3.3 指针与 nil

1 | var up *User // 声明指针,未初始化 |
这是 Go 中最常见的错误之一。
nil pointer dereference意思是在 nil 上访问字段或方法。
其实就是只声明但是没有初始化,那就不能用,cpp里面也是这样的。如果这个up不是user*的指针类型而是user类型的话,那就没事儿,可以直接赋值
3.4 结构体实现接口
1 | package main |

核心场景:在 Go 语言中,结构体通过实现接口定义的所有方法,来间接完成「接口实现」。它是 Go 实现“多态”的基石。
1. 核心原则:什么是 Go 的“鸭子类型”(结合幻灯片 1)
在 Java 或 C++ 中,如果你想让一个类实现某个接口,必须显式写上 implements 关键字。
但 Go 语言完全抛弃了这种写法,遵循经典的**鸭子类型(Duck Typing)**逻辑:
“当看到某个东西走起路来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这个东西就可以被称为鸭子。”

映射到 Go 的语法中就是:
一个结构体不需要显式声明它实现了哪个接口。只要它恰好拥有该接口中定义的所有方法,它自动就实现了这个接口。
2. 手动实现 vs IDE 一键生成(结合幻灯片 2、3 和你的代码)
假设我们有一个 List 接口,以及一个名为 LinkedList 的结构体:
第一步:定义接口
1 | type List interface { |
第二步:定义结构体
1 | type LinkedList struct { |
第三步:让结构体实现接口(两种方式)
方式 A:手写(传统做法)
自己手动给LinkedList绑定与接口签名一致的方法:1
2
3
4
5
6
7
8
9func (l *LinkedList) Add(idx int, val any) {
// 真正的逻辑:插入节点
}
func (l *LinkedList) Append(val any) {
// 真正的逻辑:追加节点
}
func (l *LinkedList) Delete(idx int) {
// 真正的逻辑:删除节点
}方式 B:IDE 快捷生成

在 GoLand 或 VS Code 中,在结构体名字上右键 →
Generate→ 选择Implement Methods...。
IDE 会弹窗让你选择要实现哪个接口。选中后,IDE 会自动帮你生成如下骨架代码:1
2
3
4
5func (l *LinkedList) Add(idx int, val any) {
// TODO implement me
panic("implement me") // ⚠️ 这是一个占位符,实际运行时会直接崩掉!
}
// Append 和 Delete 同理...⚠️ 实战警告:IDE 生成的
panic("implement me")仅供占位。在正式把代码提交测试前,必须删掉这行panic,替换成真实的业务逻辑,否则程序一运行到这里就会直接崩溃!
第四步:接口与结构体的多态绑定
在代码中,*LinkedList 虽然没有写 implements List,但因为它的方法完全匹配,它可以无缝传递给需要 List 类型的地方:
1 | // 接收 List 接口作为参数 |
3. 细节纠偏:为什么接收器必须是 (l *LinkedList) 而不是 (l LinkedList)?
结合笔记前面提到的 “方法接收器” 知识,这一点至关重要:
- 因为
LinkedList内部包含指针(操作的是链表节点内存)。 - 如果接收器写成
(l LinkedList)(值接收器),调用方法时 Go 会把整个链表深拷贝一份。你往副本里插入节点,外部的myList根本不会变。 - 因此,必须使用指针接收器
(l \*LinkedList)。
引出的一个陷阱:
因为接收器是指针接收器,所以你在传递给 UseList 时,必须传递取地址后的对象,也就是 &LinkedList{}。
如果你直接传值 var v LinkedList; UseList(v),编译器会直接报错:
LinkedList does not implement List (method Add has pointer receiver)。因为v是一个值类型,它永远无法拥有指针接收器的方法。
4. 底层原理:Go 接口与 C++ 模板的区别(解除困惑)
| 语言对比 | C++ 模板 | Go 接口 |
|---|---|---|
| 类型逻辑 | 编译期多态。编译器针对 int、string 等具体类型分别生成独立的二进制代码。 |
运行时多态。接口变量在运行时动态持有具体的结构体实例,本质类似于 C++ 的 virtual 虚函数。 |
| 代码膨胀 | 使用模板会导致编译出的二进制文件变大(因为生成了多份代码)。 | 接口使用的代码量基本恒定,不会因为类型变多而膨胀。 |
| 类比 | 像是一个超级打印机的模具,每个不同材质都要压一张新纸。 | 像是一个通用插排,甭管你是手机充电器还是电脑插头,只要插头形状(方法签名)合适,插上就能用。 |
5. 避坑指南与黄金法则
- 接口只看“能力”,不看“出身”:只要方法列表匹配,用户自定义的结构体,甚至别人的第三方库结构体,都可以直接当成你的接口使用,无需修改第三方库。
- 方法签名严格对应:必须完全拷贝接口里的方法名、参数类型、返回值类型,少一个参数或多一个返回值,都无法实现接口。
- 慎用
any(interface{}):如果接口的方法里用了any,虽然看着通用,但接收方需要频繁做类型断言(强制类型转换),会丢失 Go 的类型安全检查。实战中,通常推荐配合泛型(如[T any])来使用。
6. 总结
一句口诀带你记住 Go 的结构体与接口:
接口定行为,结构体备方法。全都有,自动合;少一个,报编译错。
为了改数据,接收器永远优先选指针!
7.如果还是不理解就看下面的例子吧
1 | package main |
底层原理解析(多态的“魔法”所在)
结合你前面的笔记,你可以这样理解这个案例:
- 没有写
implements也能实现多态:在 Java 里你可能得写class Dog implements Animal。但在 Go 里面,因为Dog和Cat都有Speak() string这个方法,它们自动就变成了Animal。这就是我们说的鸭子类型(只要走起路来像鸭子,叫起来像鸭子,它就是鸭子)。 - 底层是怎么动起来的?
当LetAnimalSpeak(myDog)运行时,Go 的底层会把myDog装进一个Animal类型的“接口盒子”里。这个盒子里既存了具体的对象(旺财),也存了对象的方法指针(狗的Speak方法在哪里)。
当程序执行a.Speak()时,它会去盒子里找具体是谁在叫,然后动态分发给对应的代码执行。这就是经典的运行时多态。
结合刚讲的内容,给你一个“避坑提醒”
如果你的方法接收器用了指针,比如 func (d *Dog) Speak() string:
1 | // 接收器改成指针 |
那么在 main 函数里,不能直接传 myDog(值类型)给 LetAnimalSpeak()。编译器会报错:
1 | cannot use myDog (variable of type Dog) as Animal value in argument to LetAnimalSpeak: Dog does not implement Animal (method Speak has pointer receiver) |
怎么解?
遇事不决用指针。在创建对象时就直接用指针,或者调用的时候取地址:
1 | myDog := &Dog{Name: "旺财"} // 创建指针 |
小建议:在实际后端开发里,如果一个结构体里有切片、Map 或者你需要修改它的状态,强烈建议统一用指针接收器 (\*Type) 来实现接口,这样不仅多态能用,还能避免值拷贝带来的内存浪费和数据无法修改的坑。
四、方法接收器(Method Receiver)

4.1 两种接收器
1 | // 值接收器 |
4.2 核心区别

1 | u1 := User{Name: "Tom", Age: 18} |
原理:
1 | 值接收器: |
4.3 黄金法则
遇事不决,用指针!
- 用指针最多遇到 nil 指针,一眼能看出来
- 用值接收器可能想改数据却改不了,debug 半天找不到原因
- 例外:不可变对象设计(后面课程会总结)
4.4 互相调用(语法糖)
Go 编译器会自动帮你转换:
1 | // 结构体可以调指针方法 |
4.5 结构体自引用必须用指针

1 | // 编译错误:invalid recursive type |
五、组合(Composition)

5.1 Go 没有继承,只有组合
1 | type User struct { |
5.2 组合的效果
1 | s := Student{ |
本质:组合就是把一个结构体嵌到另一个里面,字段会被「提升」到外层,可以直接访问。
5.3 模拟「继承」

输出的是hello,Inner,而不是outer
1 | // 定义基础行为 |
注意:这不是真正的继承,Go 没有多态,但通过接口可以实现类似效果。
六、衍生类型与类型别名
6.1 衍生类型(Defined Type)
一、基本概念与特性

Go 语言中,通过 type 关键字,可以利用一个已有的类型(基础类型或结构体)定义出一个全新的类型。
1 | type Fish struct { Age int } |
核心原则: 衍生类型在底层内存布局上与原类型完全一致,但在编译器的类型系统中,两者是彻底独立、互不相干的两种类型。
f2不能调用fish的方法,但是可以调用fish里面的字段
1 | type Integer int // Integer 是新类型,和 int 不同 |
特点:
- 和原类型是不同类型,需要显式转换
- 可以为它定义自己的方法
1 | func (i Integer) Double() Integer { |
二、与 C++ 等传统语言的对比
很多初学者会把它和 C++ 的概念混淆,这里做一下对比:
| 语言 | 写法 | 特点 |
|---|---|---|
C++ (typedef) |
typedef int MyInt; |
仅是别名,MyInt 和 int 在任何地方都可以互换使用。 |
| C++ (继承) | class B : public A {} |
继承方法和数据,B 自动拥有 A 的所有属性和方法,关系紧密。 |
| Go (衍生类型) | type MyInt int |
全新独立类型,底层内存一样,但绑定方法完全不同,需显式强转才能互转。 |
三、衍生类型的核心用途(到底有什么用?)
真实开发中,衍生类型主要有以下三大应用场景:
1. 实现编译时的”类型安全”(防止鸡同鸭讲)
利用类型隔离,防止混用不同含义的数据。
1 | type Celsius float64 |
2. 给基础类型(int、string)增加方法
Go 语言不允许给基础类型直接绑定方法,但通过衍生类型可以”曲线救国”。
1 | type MyInt int |
3. 扩展第三方库
无法修改第三方库源码,但想借用它的数据结构并扩展自己的方法。
错误做法: 试图在外部包给第三方结构体写方法
1
func (f SomePkg.Fish) Swim() // ❌ 编译报错:不能为远程包的类型定义新方法。
正确做法: 在自己的包内定义衍生类型
type MyFish SomePkg.Fish,然后把原类型的数据”映射”进来,再给自己的类型加方法。
四、字段与方法的底层逻辑
1. 字段的访问与转换(数据拷贝)
衍生类型之间可以互相转换,但遵循值拷贝(浅拷贝)原则:
1 | type Fish struct { Age int } |
⚠️ 隐藏陷阱(如果是切片 / Map): 如果结构体里面有切片或者 Map,转换时底层数组/哈希表是共享的。如果修改了
f3切片里面的元素,f2读到的数据也会改变。修改切片长度或重新赋值则不影响。
2. 方法不共享,需要独立绑定
衍生类型不会继承原类型的方法。
Fish身上的方法,FakeFish统统没有。- 需要给
FakeFish重新定义它自己的方法:
1 | // 给全新类型 FakeFish 绑定全新方法 |
互转调用: 如果要用原类型 Fish 的方法,必须把衍生类型强转回去:Fish(f2).SomeMethod()。
五、关于接口的重要陷阱
最后的警告非常关键:衍生类型实现了接口,不等于原类型也实现了接口。
1 | type Speaker interface { Speak() string } |
六、总结(一句话看懂 Go 的设计哲学)
Go 使用 type 衍生类型,意在把「数据(字段)」和「行为(方法)」彻底解耦。
你完全可以使用底层相同的内存结构(数据),通过强转借用过来;同时抛弃旧的行为,重新定义一套属于你自己的新方法。这种设计避免了传统 OOP 语言中复杂的类继承层级和多态重写带来的混乱,代码更加安全、克制且清晰。
6.2 类型别名(Type Alias)

1 | type MyInt = int // MyInt 就是 int,完全等价 |
特点:
- 和原类型完全等价
- 只是换了个名字
- 使用场景:向后兼容、代码迁移
6.3 对比
| 特性 | 衍生类型 type A B |
类型别名 type A = B |
|---|---|---|
| 是否新类型 | 是 | 不是 |
| 方法 | 独立 | 共享 |
| 转换 | 需要显式转换 | 不需要 |
七、泛型(Generics)

**[T any] 在语法上,就相当于 C++ 里的 template <typename T>。**所以T是可以随便改的,可以改成 M any,y any等等
7.1 为什么需要泛型
没有泛型的问题:any 什么都能放,完全控制不住类型
1 | type List struct { |
泛型解决:约束类型,编译期就能发现问题
1 | type List[T any] struct { |
7.2 泛型基本语法
结构体泛型
1 | type LinkedList[T any] struct { |

方法泛型
1 | func Sum[T any](nums ...T) T { |

7.3 泛型约束
any 太宽泛,不能做运算。需要类型约束。
1 | // 编译错误:any 不支持 + |
解决方案:定义约束接口
1 | // 定义一个「数字」约束 |
7.4 ~ 符号的含义
~ 表示包含该类型的衍生类型。
1 | type Integer int // 衍生类型 |
7.5 常用内置约束
1 | // any:任意类型(等价于 interface{}) |
八、第一周总结
8.1 核心知识点
| 主题 | 关键内容 |
|---|---|
| 变量与常量 | 大小写控制可见性、iota |
| 方法 | 多返回值、defer、闭包 |
| 控制结构 | if/for/switch、for-range 陷阱 |
| 内置类型 | 数组(基本不用)、切片(底层共享)、map(遍历随机) |
| 接口 | 行为抽象、面向接口编程 |
| 结构体 | 初始化、指针、方法接收器 |
| 组合 | 代码复用、Go 没有继承 |
| 泛型 | 类型参数、约束、~ 符号 |
8.2 重要原则
- 大小写控制可见性:大写 = 导出,小写 = 私有
- 遇事不决用指针:避免值传递的坑
- 面向接口编程:声明用接口,实现用结构体
- 组合而非继承:Go 没有继承,只有组合
8.3 常见错误速查
| 错误 | 原因 | 解决 |
|---|---|---|
no new variables |
:= 左边没有新变量 |
至少有一个新变量 |
nil pointer dereference |
空指针访问 | 检查是否为 nil |
invalid recursive type |
结构体自引用没用指针 | 改成指针 |
for-range 取地址 |
迭代变量地址相同 | 用索引访问 |
| 值接收器改不了数据 | 改的是副本 | 改用指针接收器 |










