GO学习笔记 | 第三章节 GO语言基础 | 接口&&结构体&&方法接收器&&组合&&泛型
核心内容 :接口、结构体、方法接收器、组合、衍生类型与类型别名、泛型
前置知识:方法声明、函数式编程、控制结构(if/for/switch)、内置类型
文章目录
- [GO学习笔记 | 第三章节 GO语言基础 | 接口&&结构体&&方法接收器&&组合&&泛型](#GO学习笔记 | 第三章节 GO语言基础 | 接口&&结构体&&方法接收器&&组合&&泛型)
-
- [一、课前回顾:defer 与闭包](#一、课前回顾:defer 与闭包)
-
- [1.1 for 循环中 defer 的经典问题](#1.1 for 循环中 defer 的经典问题)
- 二、接口(Interface)
-
- [2.1 什么是接口](#2.1 什么是接口)
- [2.2 接口特点](#2.2 接口特点)
- [2.3 接口设计原则](#2.3 接口设计原则)
- 三、结构体(Struct)
-
- [3.1 结构体定义](#3.1 结构体定义)
- [3.2 结构体初始化](#3.2 结构体初始化)
- [3.3 指针与 nil](#3.3 指针与 nil)
- [3.4 结构体实现接口](#3.4 结构体实现接口)
-
- [1. 核心原则:什么是 Go 的"鸭子类型"(结合幻灯片 1)](#1. 核心原则:什么是 Go 的“鸭子类型”(结合幻灯片 1))
- [2. 手动实现 vs IDE 一键生成(结合幻灯片 2、3 和你的代码)](#2. 手动实现 vs IDE 一键生成(结合幻灯片 2、3 和你的代码))
- [3. 细节纠偏:为什么接收器必须是 `(l *LinkedList)` 而不是 `(l LinkedList)`?](#3. 细节纠偏:为什么接收器必须是
(l *LinkedList)而不是(l LinkedList)?) - [4. 底层原理:Go 接口与 C++ 模板的区别(解除困惑)](#4. 底层原理:Go 接口与 C++ 模板的区别(解除困惑))
- [5. 避坑指南与黄金法则](#5. 避坑指南与黄金法则)
- [6. 总结](#6. 总结)
- 7.如果还是不理解就看下面的例子吧
- [四、方法接收器(Method Receiver)](#四、方法接收器(Method Receiver))
-
- [4.1 两种接收器](#4.1 两种接收器)
- [4.2 核心区别](#4.2 核心区别)
- [4.3 黄金法则](#4.3 黄金法则)
- [4.4 互相调用(语法糖)](#4.4 互相调用(语法糖))
- [4.5 结构体自引用必须用指针](#4.5 结构体自引用必须用指针)
- 五、组合(Composition)
-
- [5.1 Go 没有继承,只有组合](#5.1 Go 没有继承,只有组合)
- [5.2 组合的效果](#5.2 组合的效果)
- [5.3 模拟「继承」](#5.3 模拟「继承」)
- 六、衍生类型与类型别名
-
- [6.1 衍生类型(Defined Type)](#6.1 衍生类型(Defined Type))
-
- 一、基本概念与特性
- [二、与 C++ 等传统语言的对比](#二、与 C++ 等传统语言的对比)
- 三、衍生类型的核心用途(到底有什么用?)
-
- [1. 实现编译时的"类型安全"(防止鸡同鸭讲)](#1. 实现编译时的"类型安全"(防止鸡同鸭讲))
- [2. 给基础类型(`int`、`string`)增加方法](#2. 给基础类型(
int、string)增加方法) - [3. 扩展第三方库](#3. 扩展第三方库)
- 四、字段与方法的底层逻辑
-
- [1. 字段的访问与转换(数据拷贝)](#1. 字段的访问与转换(数据拷贝))
- [2. 方法不共享,需要独立绑定](#2. 方法不共享,需要独立绑定)
- 五、关于接口的重要陷阱
- [六、总结(一句话看懂 Go 的设计哲学)](#六、总结(一句话看懂 Go 的设计哲学))
- [6.2 类型别名(Type Alias)](#6.2 类型别名(Type Alias))
- [6.3 对比](#6.3 对比)
- 七、泛型(Generics)
- 八、第一周总结
-
- [8.1 核心知识点](#8.1 核心知识点)
- [8.2 重要原则](#8.2 重要原则)
- [8.3 常见错误速查](#8.3 常见错误速查)
一、课前回顾:defer 与闭包
1.1 for 循环中 defer 的经典问题
go
for i := 0; i < 10; i++ {
defer func() {
fmt.Println(i) // 为什么输出的都是 10?
}()
}
原因 :i 的地址始终是同一个,defer 延迟到函数返回前才执行,此时 i 已经是 10 了。
验证:打印地址确认
go
for i := 0; i < 10; i++ {
fmt.Printf("i 的地址: %p\n", &i) // 每次循环地址相同!
defer func() {
fmt.Println(i) // 全是 10
}()
}
两种解决方案:
go
// 方案1:传参(值传递,推荐)
for i := 0; i < 10; i++ {
defer func(v int) {
fmt.Println(v) // 9, 8, 7, ..., 0
}(i)
}
// 方案2:每次循环创建新变量
for i := 0; i < 10; i++ {
j := i // 每次循环都是全新的 j
defer func() {
fmt.Println(j) // 9, 8, 7, ..., 0
}()
}
二、接口(Interface)

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

go
type User struct {
Name string
Age int
}
type LinkedList struct {
head *Node // 指针类型
tail *Node
}
type Node struct {
value any
next *Node
prev *Node
}
访问控制:大写开头 = 包外可访问,小写开头 = 包内私有
3.2 结构体初始化

方式1:直接初始化(拿到结构体)
go
u1 := User{} // 零值初始化
u2 := User{
Name: "Tom",
Age: 18,
}
方式2:取地址(拿到指针)
go
up1 := &User{
Name: "Tom",
Age: 18,
}
up2 := new(User) // 等价于 &User{} 都是零值

打印结构体
go
u := User{Name: "Tom", Age: 18}
fmt.Printf("%v\n", u) // {Tom 18}
fmt.Printf("%+v\n", u) // {Name:Tom Age:18} 推荐,带字段名
3.3 指针与 nil

go
var up *User // 声明指针,未初始化
fmt.Println(up) // <nil>
// 空指针访问会 panic
up.Name = "Tom" // panic: invalid memory address or nil pointer dereference
这是 Go 中最常见的错误之一 。
nil pointer dereference意思是在 nil 上访问字段或方法。
其实就是只声明但是没有初始化,那就不能用,cpp里面也是这样的。如果这个up不是user*的指针类型而是user类型的话,那就没事儿,可以直接赋值
3.4 结构体实现接口
go
package main
import "fmt"
// 1. 定义一个接口
type List interface {
Add(idx int, val any)
Append(val any)
Delete(idx int)
}
// 2. 定义你的结构体
type LinkedList struct {
head *node // 链表的头节点
}
type node struct {
val any
next *node
}
// 3. 手动实现接口要求的方法(或者用图2的IDE快捷键自动生成)
func (l *LinkedList) Add(idx int, val any) {
// 实际写真正的链表插入逻辑,而不是 panic
fmt.Printf("在下标 %d 插入值 %v\n", idx, val)
}
func (l *LinkedList) Append(val any) {
fmt.Printf("在末尾追加值 %v\n", val)
}
func (l *LinkedList) Delete(idx int) {
fmt.Printf("删除了下标 %d 的元素\n", idx)
}
// 4. 见证奇迹的时刻:尽管我们没有写 "implements List"
// 但因为 *LinkedList 拥有所有方法,它自动实现了 List 接口!
func UseList(list List) {
list.Append(100)
list.Delete(0)
}
func main() {
// 我们直接可以把 *LinkedList 传给 UseList
myList := &LinkedList{}
UseList(myList) // 输出:在末尾追加值 100 \n 删除了下标 0 的元素
}

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

映射到 Go 的语法中就是:
一个结构体不需要显式声明 它实现了哪个接口。只要它恰好拥有 该接口中定义的所有方法,它自动就实现了这个接口。
2. 手动实现 vs IDE 一键生成(结合幻灯片 2、3 和你的代码)
假设我们有一个 List 接口,以及一个名为 LinkedList 的结构体:
第一步:定义接口
go
type List interface {
Add(idx int, val any)
Append(val any)
Delete(idx int)
}
第二步:定义结构体
go
type LinkedList struct {
head *node
}
type node struct {
val any
next *node
}
第三步:让结构体实现接口(两种方式)
-
方式 A:手写(传统做法)
自己手动给
LinkedList绑定与接口签名一致的方法:gofunc (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 会自动帮你生成如下骨架代码:
gofunc (l *LinkedList) Add(idx int, val any) { // TODO implement me panic("implement me") // ⚠️ 这是一个占位符,实际运行时会直接崩掉! } // Append 和 Delete 同理...⚠️ 实战警告 :IDE 生成的
panic("implement me")仅供占位。在正式把代码提交测试前,必须删掉这行panic,替换成真实的业务逻辑,否则程序一运行到这里就会直接崩溃!
第四步:接口与结构体的多态绑定
在代码中,*LinkedList 虽然没有写 implements List,但因为它的方法完全匹配,它可以无缝传递给需要 List 类型的地方:
go
// 接收 List 接口作为参数
func UseList(list List) {
list.Append(100)
list.Delete(0)
}
func main() {
// 我们直接可以把 *LinkedList 传给 UseList
myList := &LinkedList{}
UseList(myList) // ✅ 编译通过,运行成功
}
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.如果还是不理解就看下面的例子吧
go
package main
import "fmt"
// 1. 定义一个"动物"的行为规范(接口)
// 不管是猫还是狗,它们都有"叫"的能力
type Animal interface {
Speak() string
}
// 2. 定义具体的动物结构体(狗和猫)
type Dog struct {
Name string
}
type Cat struct {
Name string
}
// 3. 让狗和猫实现 Animal 接口(绑定方法)
// 狗叫的方法
func (d Dog) Speak() string {
return fmt.Sprintf("%s: 汪汪汪!", d.Name)
}
// 猫叫的方法
func (c Cat) Speak() string {
return fmt.Sprintf("%s: 喵喵喵~", c.Name)
}
// 4. 编写一个面向接口的函数(多态的体现)
// 这个函数不需要关心传进来的是狗还是猫,它只关心对方是不是 Animal
func LetAnimalSpeak(a Animal) {
// 调用接口的方法,Go 会动态帮你分发到具体的实现上
fmt.Println(a.Speak())
}
func main() {
// 创建具体的对象
myDog := Dog{Name: "旺财"}
myCat := Cat{Name: "咪咪"}
// 把具体对象传给接口函数
LetAnimalSpeak(myDog) // 输出:旺财: 汪汪汪!
LetAnimalSpeak(myCat) // 输出:咪咪: 喵喵喵~
}
底层原理解析(多态的"魔法"所在)
结合你前面的笔记,你可以这样理解这个案例:
- 没有写
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:
go
// 接收器改成指针
func (d *Dog) Speak() string { ... }
那么在 main 函数里,不能 直接传 myDog(值类型)给 LetAnimalSpeak()。编译器会报错:
go
cannot use myDog (variable of type Dog) as Animal value in argument to LetAnimalSpeak: Dog does not implement Animal (method Speak has pointer receiver)
怎么解?
遇事不决用指针。在创建对象时就直接用指针,或者调用的时候取地址:
go
myDog := &Dog{Name: "旺财"} // 创建指针
LetAnimalSpeak(myDog) // 完美通过
// 或者
myDog := Dog{Name: "旺财"}
LetAnimalSpeak(&myDog) // 使用 & 取地址,完美通过
小建议 :在实际后端开发里,如果一个结构体里有切片、Map 或者你需要修改它的状态,强烈建议统一用指针接收器 (\*Type) 来实现接口,这样不仅多态能用,还能避免值拷贝带来的内存浪费和数据无法修改的坑。
四、方法接收器(Method Receiver)

4.1 两种接收器
go
// 值接收器
func (u User) ChangeName(name string) {
u.Name = name // 修改的是副本,原对象不变!
}
// 指针接收器
func (u *User) ChangeAge(age int) {
u.Age = age // 修改的是原对象
}
4.2 核心区别

go
u1 := User{Name: "Tom", Age: 18}
u1.ChangeAge(35) // 指针接收器 → 可以修改
fmt.Println(u1.Age) // 35
u1.ChangeName("Jerry") // 值接收器 → 修改不了!
fmt.Println(u1.Name) // 还是 "Tom"
原理:
值接收器:
u1.ChangeName("Jerry")
→ 复制一个 u1 的副本 → 修改副本 → 原 u1 不变
指针接收器:
u1.ChangeAge(35)
→ 复制指针(指向同一个 u1)→ 修改原 u1 → 原 u1 改变
4.3 黄金法则
遇事不决,用指针!
- 用指针最多遇到 nil 指针,一眼能看出来
- 用值接收器可能想改数据却改不了,debug 半天找不到原因
- 例外:不可变对象设计(后面课程会总结)
4.4 互相调用(语法糖)
Go 编译器会自动帮你转换:
go
// 结构体可以调指针方法
u := User{}
u.ChangeAge(18) // 编译器自动转: (&u).ChangeAge(18)
// 指针可以调结构体方法
up := &User{}
up.ChangeName("Tom") // 编译器自动转: (*up).ChangeName("Tom")
4.5 结构体自引用必须用指针

go
// 编译错误:invalid recursive type
type Node struct {
value any
next Node // 不能直接包含自己!
}
// 正确:用指针
type Node struct {
value any
next *Node // 指针可以
}
五、组合(Composition)

5.1 Go 没有继承,只有组合
go
type User struct {
Name string
Age int
}
// 组合:把 User 嵌入到别的结构体
type Student struct {
User // 组合 User
Grade string
Class string
}
5.2 组合的效果
go
s := Student{
User: User{Name: "Tom", Age: 18},
Grade: "大一",
Class: "1班",
}
// 可以直接访问组合进来的字段
fmt.Println(s.Name) // "Tom"(不用 s.User.Name)
fmt.Println(s.Age) // 18
fmt.Println(s.Grade) // "大一"
本质:组合就是把一个结构体嵌到另一个里面,字段会被「提升」到外层,可以直接访问。
5.3 模拟「继承」

输出的是hello,Inner,而不是outer
go
// 定义基础行为
type Base struct{}
func (b Base) SayHello() {
fmt.Println("Hello from Base")
}
// 子结构体组合 Base
type Child struct {
Base
}
func main() {
c := Child{}
c.SayHello() // "Hello from Base"(直接调用 Base 的方法)
}
注意:这不是真正的继承,Go 没有多态,但通过接口可以实现类似效果。
六、衍生类型与类型别名
6.1 衍生类型(Defined Type)
一、基本概念与特性

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

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

**[T any] 在语法上,就相当于 C++ 里的 template <typename T>。**所以T是可以随便改的,可以改成 M any,y any等等
7.1 为什么需要泛型
没有泛型的问题 :any 什么都能放,完全控制不住类型
go
type List struct {
data []any
}
l := List{}
l.data = append(l.data, 10) // int
l.data = append(l.data, 3.14) // float64
l.data = append(l.data, "hello") // string
// 乱了!什么类型都有!
泛型解决:约束类型,编译期就能发现问题
go
type List[T any] struct {
data []T
}
l := List[int]{}
l.data = append(l.data, 10) // 可以
l.data = append(l.data, 3.14) // 编译错误!
7.2 泛型基本语法
结构体泛型
go
type LinkedList[T any] struct {
head *Node[T]
tail *Node[T]
}
type Node[T any] struct {
value T
next *Node[T]
prev *Node[T]
}

方法泛型
go
func Sum[T any](nums ...T) T {
// ...
}

7.3 泛型约束
any 太宽泛,不能做运算。需要类型约束。
go
// 编译错误:any 不支持 +
func Sum[T any](nums ...T) T {
var result T
for _, n := range nums {
result = result + n // 错误!
}
return result
}
解决方案:定义约束接口
go
// 定义一个「数字」约束
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
// 使用约束
func Sum[T Number](nums ...T) T {
var result T
for _, n := range nums {
result = result + n // 现在可以了
}
return result
}
// 调用
Sum(1, 2, 3) // int
Sum(1.5, 2.5, 3.5) // float64
7.4 ~ 符号的含义
~ 表示包含该类型的衍生类型。
go
type Integer int // 衍生类型
// 没有 ~:不包含衍生类型
func Sum1[T int | float64](nums ...T) T {}
Sum1(Integer(10)) // 编译错误
// 有 ~:包含衍生类型
func Sum2[T ~int | ~float64](nums ...T) T {}
Sum2(Integer(10)) // 可以
7.5 常用内置约束
go
// any:任意类型(等价于 interface{})
type Any interface{}
// comparable:可比较类型(支持 == 和 !=)
type Comparable interface {
comparable
}
八、第一周总结
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 取地址 |
迭代变量地址相同 | 用索引访问 |
| 值接收器改不了数据 | 改的是副本 | 改用指针接收器 |