GO学习笔记 | 第三章节 GO语言基础 | 接口&&结构体&&方法接收器&&组合&&泛型

核心内容:接口、结构体、方法接收器、组合、衍生类型与类型别名、泛型
前置知识:方法声明、函数式编程、控制结构(if/for/switch)、内置类型


一、课前回顾:defer 与闭包

1.1 for 循环中 defer 的经典问题

1
2
3
4
5
for i := 0; i < 10; i++ {
defer func() {
fmt.Println(i) // 为什么输出的都是 10?
}()
}

原因i 的地址始终是同一个,defer 延迟到函数返回前才执行,此时 i 已经是 10 了。

验证:打印地址确认

1
2
3
4
5
6
for i := 0; i < 10; i++ {
fmt.Printf("i 的地址: %p\n", &i) // 每次循环地址相同!
defer func() {
fmt.Println(i) // 全是 10
}()
}

两种解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方案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)

image-20260618193443953

2.1 什么是接口

接口是一组行为的抽象——只规定「能做什么」,不关心「怎么做的」。

1
2
3
4
5
type List interface {
Add(index int, value any)
Append(value any)
Delete(index int)
}

类比理解

  • 产品经理说:「我要一个能在特定位置增删改查的数据结构」→ 这就是接口
  • 你可以用数组实现、链表实现、跳表实现…… → 这些都是具体实现
  • 老板要结果,不关心是你做的还是同事做的 → 面向接口编程

2.2 接口特点

  • 只能包含方法,不能包含字段
  • 方法签名不需要 func 关键字
  • 方法名首字母大写 = 包外可访问,小写 = 包内私有

2.3 接口设计原则

当你怀疑要不要定义接口的时候,加上接口!

  • 声明时用接口,而不是具体实现
  • 系统的核心应该面向接口编程
  • 后续讲依赖注入时会深入讲解

三、结构体(Struct)

3.1 结构体定义

image-20260619102257676

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 结构体初始化

image-20260619102312025

方式1:直接初始化(拿到结构体)

1
2
3
4
5
6
u1 := User{}  // 零值初始化

u2 := User{
Name: "Tom",
Age: 18,
}

方式2:取地址(拿到指针)

1
2
3
4
5
6
up1 := &User{
Name: "Tom",
Age: 18,
}

up2 := new(User) // 等价于 &User{} 都是零值

image-20260619105650762

打印结构体

1
2
3
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

image-20260619105805831

1
2
3
4
5
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 结构体实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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 的元素
}

image-20260619140211896

核心场景:在 Go 语言中,结构体通过实现接口定义的所有方法,来间接完成「接口实现」。它是 Go 实现“多态”的基石。

1. 核心原则:什么是 Go 的“鸭子类型”(结合幻灯片 1)

在 Java 或 C++ 中,如果你想让一个类实现某个接口,必须显式写上 implements 关键字。
但 Go 语言完全抛弃了这种写法,遵循经典的**鸭子类型(Duck Typing)**逻辑:

“当看到某个东西走起路来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这个东西就可以被称为鸭子。”

image-20260619140032182

映射到 Go 的语法中就是:

一个结构体不需要显式声明它实现了哪个接口。只要它恰好拥有该接口中定义的所有方法,它自动就实现了这个接口。

2. 手动实现 vs IDE 一键生成(结合幻灯片 2、3 和你的代码)

假设我们有一个 List 接口,以及一个名为 LinkedList 的结构体:

第一步:定义接口

1
2
3
4
5
type List interface {
Add(idx int, val any)
Append(val any)
Delete(idx int)
}

第二步:定义结构体

1
2
3
4
5
6
7
8
type LinkedList struct {
head *node
}

type node struct {
val any
next *node
}

第三步:让结构体实现接口(两种方式)

  • 方式 A:手写(传统做法)
    自己手动给 LinkedList 绑定与接口签名一致的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func (l *LinkedList) Add(idx int, val any) {
    // 真正的逻辑:插入节点
    }
    func (l *LinkedList) Append(val any) {
    // 真正的逻辑:追加节点
    }
    func (l *LinkedList) Delete(idx int) {
    // 真正的逻辑:删除节点
    }
  • 方式 B:IDE 快捷生成

    image-20260619141439835

    在 GoLand 或 VS Code 中,在结构体名字上右键 → Generate → 选择 Implement Methods...
    IDE 会弹窗让你选择要实现哪个接口。选中后,IDE 会自动帮你生成如下骨架代码

    1
    2
    3
    4
    5
    func (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
2
3
4
5
6
7
8
9
10
11
// 接收 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 接口
类型逻辑 编译期多态。编译器针对 intstring 等具体类型分别生成独立的二进制代码。 运行时多态。接口变量在运行时动态持有具体的结构体实例,本质类似于 C++ 的 virtual 虚函数。
代码膨胀 使用模板会导致编译出的二进制文件变大(因为生成了多份代码)。 接口使用的代码量基本恒定,不会因为类型变多而膨胀。
类比 像是一个超级打印机的模具,每个不同材质都要压一张新纸。 像是一个通用插排,甭管你是手机充电器还是电脑插头,只要插头形状(方法签名)合适,插上就能用。

5. 避坑指南与黄金法则

  1. 接口只看“能力”,不看“出身”:只要方法列表匹配,用户自定义的结构体,甚至别人的第三方库结构体,都可以直接当成你的接口使用,无需修改第三方库。
  2. 方法签名严格对应:必须完全拷贝接口里的方法名、参数类型、返回值类型,少一个参数或多一个返回值,都无法实现接口。
  3. 慎用 anyinterface{}:如果接口的方法里用了 any,虽然看着通用,但接收方需要频繁做类型断言(强制类型转换),会丢失 Go 的类型安全检查。实战中,通常推荐配合泛型(如 [T any])来使用。

6. 总结

一句口诀带你记住 Go 的结构体与接口:

接口定行为,结构体备方法。全都有,自动合;少一个,报编译错。
为了改数据,接收器永远优先选指针!

7.如果还是不理解就看下面的例子吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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) // 输出:咪咪: 喵喵喵~
}
底层原理解析(多态的“魔法”所在)

结合你前面的笔记,你可以这样理解这个案例:

  1. 没有写 implements 也能实现多态:在 Java 里你可能得写 class Dog implements Animal。但在 Go 里面,因为 DogCat 都有 Speak() string 这个方法,它们自动就变成了 Animal。这就是我们说的鸭子类型(只要走起路来像鸭子,叫起来像鸭子,它就是鸭子)。
  2. 底层是怎么动起来的?
    LetAnimalSpeak(myDog) 运行时,Go 的底层会把 myDog 装进一个 Animal 类型的“接口盒子”里。这个盒子里既存了具体的对象(旺财),也存了对象的方法指针(狗的 Speak 方法在哪里)。
    当程序执行 a.Speak() 时,它会去盒子里找具体是谁在叫,然后动态分发给对应的代码执行。这就是经典的运行时多态
结合刚讲的内容,给你一个“避坑提醒”

如果你的方法接收器用了指针,比如 func (d *Dog) Speak() string

1
2
// 接收器改成指针
func (d *Dog) Speak() string { ... }

那么在 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
2
3
4
5
6
myDog := &Dog{Name: "旺财"} // 创建指针
LetAnimalSpeak(myDog) // 完美通过

// 或者
myDog := Dog{Name: "旺财"}
LetAnimalSpeak(&myDog) // 使用 & 取地址,完美通过

小建议:在实际后端开发里,如果一个结构体里有切片、Map 或者你需要修改它的状态,强烈建议统一用指针接收器 (\*Type) 来实现接口,这样不仅多态能用,还能避免值拷贝带来的内存浪费和数据无法修改的坑。


四、方法接收器(Method Receiver)

image-20260619111751323

4.1 两种接收器

1
2
3
4
5
6
7
8
9
// 值接收器
func (u User) ChangeName(name string) {
u.Name = name // 修改的是副本,原对象不变!
}

// 指针接收器
func (u *User) ChangeAge(age int) {
u.Age = age // 修改的是原对象
}

4.2 核心区别

image-20260619132507892

1
2
3
4
5
6
7
u1 := User{Name: "Tom", Age: 18}

u1.ChangeAge(35) // 指针接收器 → 可以修改
fmt.Println(u1.Age) // 35

u1.ChangeName("Jerry") // 值接收器 → 修改不了!
fmt.Println(u1.Name) // 还是 "Tom"

原理

1
2
3
4
5
6
7
值接收器:
u1.ChangeName("Jerry")
→ 复制一个 u1 的副本 → 修改副本 → 原 u1 不变

指针接收器:
u1.ChangeAge(35)
→ 复制指针(指向同一个 u1)→ 修改原 u1 → 原 u1 改变

4.3 黄金法则

遇事不决,用指针!

  • 用指针最多遇到 nil 指针,一眼能看出来
  • 用值接收器可能想改数据却改不了,debug 半天找不到原因
  • 例外:不可变对象设计(后面课程会总结)

4.4 互相调用(语法糖)

Go 编译器会自动帮你转换:

1
2
3
4
5
6
7
// 结构体可以调指针方法
u := User{}
u.ChangeAge(18) // 编译器自动转: (&u).ChangeAge(18)

// 指针可以调结构体方法
up := &User{}
up.ChangeName("Tom") // 编译器自动转: (*up).ChangeName("Tom")

4.5 结构体自引用必须用指针

image-20260619133036089

1
2
3
4
5
6
7
8
9
10
11
// 编译错误:invalid recursive type
type Node struct {
value any
next Node // 不能直接包含自己!
}

// 正确:用指针
type Node struct {
value any
next *Node // 指针可以
}

五、组合(Composition)

image-20260619140641763

5.1 Go 没有继承,只有组合

1
2
3
4
5
6
7
8
9
10
11
type User struct {
Name string
Age int
}

// 组合:把 User 嵌入到别的结构体
type Student struct {
User // 组合 User
Grade string
Class string
}

5.2 组合的效果

1
2
3
4
5
6
7
8
9
10
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 模拟「继承」

image-20260619145234690

输出的是hello,Inner,而不是outer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义基础行为
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)

一、基本概念与特性

image-20260619133350366

Go 语言中,通过 type 关键字,可以利用一个已有的类型(基础类型或结构体)定义出一个全新的类型

1
2
type Fish struct { Age int }
type FakeFish Fish // FakeFish 就是基于 Fish 衍生出来的新类型

核心原则: 衍生类型在底层内存布局上与原类型完全一致,但在编译器的类型系统中,两者是彻底独立、互不相干的两种类型。

f2不能调用fish的方法,但是可以调用fish里面的字段

1
2
3
4
5
6
7
type Integer int  // Integer 是新类型,和 int 不同

var a Integer = 10
var b int = 20

// a = b // 编译错误!类型不同,需要显式转换
a = Integer(b) // 正确

特点

  • 和原类型是不同类型,需要显式转换
  • 可以为它定义自己的方法
1
2
3
func (i Integer) Double() Integer {
return i * 2
}

二、与 C++ 等传统语言的对比

很多初学者会把它和 C++ 的概念混淆,这里做一下对比:

语言 写法 特点
C++ (typedef) typedef int MyInt; 仅是别名,MyIntint 在任何地方都可以互换使用。
C++ (继承) class B : public A {} 继承方法和数据,B 自动拥有 A 的所有属性和方法,关系紧密。
Go (衍生类型) type MyInt int 全新独立类型,底层内存一样,但绑定方法完全不同,需显式强转才能互转。

三、衍生类型的核心用途(到底有什么用?)

真实开发中,衍生类型主要有以下三大应用场景:

1. 实现编译时的”类型安全”(防止鸡同鸭讲)

利用类型隔离,防止混用不同含义的数据。

1
2
3
4
5
6
7
type Celsius float64
type Fahrenheit float64

var tempC Celsius = 100
var tempF Fahrenheit = 212

// tempC = tempF // ❌ 编译报错,类型不匹配!必须在设计层面就区分开。
2. 给基础类型(intstring)增加方法

Go 语言不允许给基础类型直接绑定方法,但通过衍生类型可以”曲线救国”。

1
2
3
4
5
6
7
8
9
type MyInt int

// 给衍生出的新类型增加方法
func (m MyInt) IsEven() bool {
return m%2 == 0
}

var n MyInt = 10
println(n.IsEven()) // ✅ 成功输出 true
3. 扩展第三方库

无法修改第三方库源码,但想借用它的数据结构并扩展自己的方法。

  • 错误做法: 试图在外部包给第三方结构体写方法

    1
    func (f SomePkg.Fish) Swim() // ❌ 编译报错:不能为远程包的类型定义新方法。
  • 正确做法: 在自己的包内定义衍生类型 type MyFish SomePkg.Fish,然后把原类型的数据”映射”进来,再给自己的类型加方法。

四、字段与方法的底层逻辑

1. 字段的访问与转换(数据拷贝)

衍生类型之间可以互相转换,但遵循值拷贝(浅拷贝)原则:

1
2
3
4
5
6
7
8
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 重新定义它自己的方法:
1
2
3
4
// 给全新类型 FakeFish 绑定全新方法
func (f FakeFish) FakeSwim() {
fmt.Println("FakeFish 专属方法")
}

互转调用: 如果要用原类型 Fish 的方法,必须把衍生类型强转回去:Fish(f2).SomeMethod()

五、关于接口的重要陷阱

最后的警告非常关键:衍生类型实现了接口,不等于原类型也实现了接口。

1
2
3
4
5
6
7
8
9
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)

image-20260619133849533

1
2
3
4
5
6
type MyInt = int  // MyInt 就是 int,完全等价

var a MyInt = 10
var b int = 20

a = b // 正确,不需要转换

特点

  • 和原类型完全等价
  • 只是换了个名字
  • 使用场景:向后兼容、代码迁移

6.3 对比

特性 衍生类型 type A B 类型别名 type A = B
是否新类型 不是
方法 独立 共享
转换 需要显式转换 不需要

七、泛型(Generics)

image-20260619145900221

**[T any] 在语法上,就相当于 C++ 里的 template <typename T>。**所以T是可以随便改的,可以改成 M any,y any等等

7.1 为什么需要泛型

没有泛型的问题any 什么都能放,完全控制不住类型

1
2
3
4
5
6
7
8
9
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
// 乱了!什么类型都有!

泛型解决:约束类型,编译期就能发现问题

1
2
3
4
5
6
7
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 泛型基本语法

结构体泛型

1
2
3
4
5
6
7
8
9
10
type LinkedList[T any] struct {
head *Node[T]
tail *Node[T]
}

type Node[T any] struct {
value T
next *Node[T]
prev *Node[T]
}

image-20260619150307367

方法泛型

1
2
3
func Sum[T any](nums ...T) T {
// ...
}

image-20260619151414226

7.3 泛型约束

any 太宽泛,不能做运算。需要类型约束

1
2
3
4
5
6
7
8
// 编译错误:any 不支持 +
func Sum[T any](nums ...T) T {
var result T
for _, n := range nums {
result = result + n // 错误!
}
return result
}

解决方案:定义约束接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个「数字」约束
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 ~ 符号的含义

~ 表示包含该类型的衍生类型

1
2
3
4
5
6
7
8
9
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 常用内置约束

1
2
3
4
5
6
7
// any:任意类型(等价于 interface{})
type Any interface{}

// comparable:可比较类型(支持 == 和 !=)
type Comparable interface {
comparable
}

八、第一周总结

8.1 核心知识点

主题 关键内容
变量与常量 大小写控制可见性、iota
方法 多返回值、defer、闭包
控制结构 if/for/switch、for-range 陷阱
内置类型 数组(基本不用)、切片(底层共享)、map(遍历随机)
接口 行为抽象、面向接口编程
结构体 初始化、指针、方法接收器
组合 代码复用、Go 没有继承
泛型 类型参数、约束、~ 符号

8.2 重要原则

  1. 大小写控制可见性:大写 = 导出,小写 = 私有
  2. 遇事不决用指针:避免值传递的坑
  3. 面向接口编程:声明用接口,实现用结构体
  4. 组合而非继承:Go 没有继承,只有组合

8.3 常见错误速查

错误 原因 解决
no new variables := 左边没有新变量 至少有一个新变量
nil pointer dereference 空指针访问 检查是否为 nil
invalid recursive type 结构体自引用没用指针 改成指针
for-range 取地址 迭代变量地址相同 用索引访问
值接收器改不了数据 改的是副本 改用指针接收器