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

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)
    • [四、方法接收器(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)
      • [7.1 为什么需要泛型](#7.1 为什么需要泛型)
      • [7.2 泛型基本语法](#7.2 泛型基本语法)
      • [7.3 泛型约束](#7.3 泛型约束)
      • [7.4 `~` 符号的含义](#7.4 ~ 符号的含义)
      • [7.5 常用内置约束](#7.5 常用内置约束)
    • 八、第一周总结
      • [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 绑定与接口签名一致的方法:

    go 复制代码
    func (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 会自动帮你生成如下骨架代码:

    go 复制代码
    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 类型的地方:

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. 避坑指南与黄金法则
  1. 接口只看"能力",不看"出身":只要方法列表匹配,用户自定义的结构体,甚至别人的第三方库结构体,都可以直接当成你的接口使用,无需修改第三方库。
  2. 方法签名严格对应:必须完全拷贝接口里的方法名、参数类型、返回值类型,少一个参数或多一个返回值,都无法实现接口。
  3. 慎用 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) // 输出:咪咪: 喵喵喵~
}
底层原理解析(多态的"魔法"所在)

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

  1. 没有写 implements 也能实现多态 :在 Java 里你可能得写 class Dog implements Animal。但在 Go 里面,因为 Dog 和 Cat 都有 Speak() string 这个方法,它们自动 就变成了 Animal。这就是我们说的鸭子类型(只要走起路来像鸭子,叫起来像鸭子,它就是鸭子)。
  2. 底层是怎么动起来的?
    当 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. 扩展第三方库

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

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

    go 复制代码
    func (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 重要原则

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

8.3 常见错误速查

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