go语法总结
Go 语言变量声明笔记
在 Go 语言中,变量的声明有多种不同的方式。
⚠️ 核心规则:
在 Go 语言中,声明的变量定义了就必须被使用,否则在编译时会报错(declared and not used)。
1. 标准声明 (指定变量类型)
使用 var 关键字,明确指定变量的类型。先声明,后赋值。
1 | // 声明一个 int 类型的变量 age |
2. 批量声明多个同类型变量
可以在同一行声明多个相同类型的变量,然后再进行赋值。
1 | // 声明两个 string 类型的变量 |
3. 类型推断 (省略变量类型)
在声明并赋初值时,可以省略类型。Go 编译器会根据等号右边的值自动推断变量的类型。
1 | // 编译器自动推断 age2 为 int 类型 |
4. 简短变量声明 ( := 语法)
这是 Go 语言中最常用的局部变量声明方式。省略 var 关键字和类型,直接使用 := 进行声明和初始化。
注意:这种方式只能用在函数内部,不能用于定义全局变量。
1 | // 自动推断 name4 为 string 类型 |
5. 简短声明多个不同类型的变量
使用 := 可以同时声明和初始化多个不同类型的变量,非常方便。
1 | // 同时声明并初始化 string, bool, int 类型的变量 |
常量
在 Go 语言中,常量是指在程序编译阶段就确定下来,且在程序运行期间其值不能被改变的量。使用 const 关键字进行定义。
1. 常量的基本声明
常量可以通过 const 关键字声明,可以指定类型,也可以让编译器自动推断类型。
1 | // 指定类型 |
2. 批量声明常量
与变量类似,常量也支持使用圆括号进行批量声明。这在组织一组相关常量时非常清晰,常常用于定义配置项或全局状态。
1 | const ( |
也可以在同一行平行声明多个不同类型的常量:
1 | const a, b, c = 1, "like", 3 |
3. 常量的核心特性 ⚠️
- 不可修改: 常量一旦声明并赋值,在程序运行期间绝对不能被修改。
- 不可将变量赋值给常量: 常量的值必须在编译时就能确定。你不能将一个运行期间才得出结果的变量赋值给常量。
- 支持部分内置函数: 如果内置函数的参数在编译时是固定的,那么它的结果就可以用于常量赋值。例如
len()函数如果测量的是字符串字面量,就可以赋值给常量。
1 | // ✅ 正确:字符串 "abc" 的长度在编译时就固定是 3 |
4. 使用常量模拟枚举 (Enum)
Go 语言没有专门的 enum 关键字,但通常使用 const 块来定义一组相关的枚举值,例如状态码。
1 | const ( |
Go 语言 iota 计数器
在 Go 语言中,没有专门的 enum 关键字。我们通常通过 const 配合特殊的常量计数器 iota 来优雅地实现枚举的功能。
⚠️ 核心规则:
iota只能在const内部使用。- 每次遇到
const关键字时,iota都会被重置为0。const块中每新增一行常量声明,iota的值就会自动加1。
1. 基础用法 (从 0 开始递增)
最直观的写法是每一行都显式地给常量赋值 iota。
1 | const ( |
2. 简写模式 (隐式递增)
在 const 块中,如果后续的常量没有显式赋值,它会默认重复上一行的表达式。因此,实际开发中我们通常只需要在第一行写一次 iota。
1 | const ( |
3. 跳过特定的值 (使用空白标识符 _)
如果在枚举的序列中,你希望跳过某个特定的数字,可以使用空白标识符 _ 来占位。此时 iota 依然会随着行号的增加而在后台照常递增。
1 | const ( |
4. 同一行声明多个常量
极其重要: iota 的递增是严格基于代码行数的,而不是变量的个数。如果在一行内同时定义多个变量,它们引用的 iota 值是相同的。
1 | const ( |
基础数据类型
1. 布尔类型 (Boolean)
布尔类型用于表示逻辑上的真和假,其值只允许是 true 或 false。
⚠️ 核心规则:极其严谨的布尔值
与 C、C++ 或 Python 等语言不同,在 Go 语言中,**数字1和0绝对不能当做布尔值使用,也不会自动转换为true和false**。布尔类型和数字类型是完全隔离的。
1 | // 1. 显式声明与隐式类型推断 |
2. 整数类型 (Integer)
Go 语言提供了极其丰富的整数类型,主要分为两大类:平台相关 和 固定大小。
- 平台相关类型 (
int/uint): 这也是我们最常用的类型。它的大小会随着您的操作系统架构而变化。在 32 位系统上它是 32 位,在 64 位系统上它是 64 位。 - 固定大小类型 (
int8/int16/int32/int64): 无论代码运行在什么机器上,它们在内存中占用的位数都是固定不变的。这在网络传输或处理二进制文件时非常重要。 - 无符号整数 (
uint): 带u前缀的代表 Unsigned(无符号),只能存储 0 和正数,绝对不能存负数。
1 | var a int = 10 // 大小随操作系统变化 (常用) |
3. 浮点数类型 (Float)
Go 语言中没有 double 关键字,小数统称为浮点数,分为单精度和双精度两种。
float32: 单精度浮点数。float64: 双精度浮点数,精度更高。
💡 默认类型提示:
如果您在声明时省略了类型(例如f := 3.14或var f = 3.14),Go 编译器会**默认将其推断为float64**。在日常开发中,为了避免精度丢失问题,官方也推荐优先使用float64。
1 | var a float64 = 3.14 // 双精度 (Go的默认浮点类型) |
字符与字符串
1. 字符类型 (Characters)
在 Go 语言中,字符不是独立的特殊类型,而是由特定的整数类型来表示的。字符必须使用**单引号 ''** 包裹。
byte(等同于uint8):主要用于存储 ASCII 字符(英文字母、数字、标点等)。
限制: 无法存储中文字符,因为一个中文字符通常占用 3 个字节,而
byte只有 1 个字节。rune(等同于int32):用于存储 Unicode 字符。
优势: 可以完美容纳中文、日文、韩文等各种复杂字符。
格式化输出:
直接使用
fmt.Println()打印字符变量,输出的是它的数字编码。如果想看到真正的字符长什么样,必须使用
fmt.Printf("%c", 变量名)。
1 | // byte 示例 |
2. 字符串类型 (Strings)
字符串是由一系列字符连接而成的序列。
- 普通字符串: 使用**双引号
""**包裹,支持转义字符(如\n,\t)。 - 原生字符串: “里面
\失去作用,适合 sql, html 等”,在 Go 语言中使用**反引号 ``**` (键盘左上角 Esc 键下方)来表示的,原生字符串会原封不动地保留格式和特殊符号。
⚠️ 核心特性:字符串不可变 (Immutable)
Go 语言中的字符串一旦定义,就绝对不能直接修改其中的某个字符。
1 | s := "hello" |
💡 如何强制修改字符串?
如果必须修改,需要进行类型转换:先将字符串转换成 []rune (rune 切片),修改切片里的内容,然后再转回 string。
1 | var s = "张三" |
3. 字符串拼接的三种常用方式
根据不同的场景,Go 语言提供了多种拼接字符串的方法:
- 方式一:使用
+加号(最简单直观)
适合少量、简单的字符串拼接。
1 | s3 := "hello" + " " + "world" |
- 方式二:使用
fmt.Sprintf(格式化拼接)
适合需要将变量和字符串混合排版的场景。
1 | s1, s2 := "hello", "world" |
- 方式三:使用
strings.Builder(性能最高 🚀)
极力推荐: 在需要频繁、大量拼接字符串时(例如在循环内部),使用strings.Builder是性能最好、内存消耗最低的方式。
1 | var builder strings.Builder |
运算符与控制流
1. 运算符的核心特异点 ⚠️
Go 语言的运算符与 C/Java 等语言大致相通,但有几个极其严格的限制:
- 极其严格的类型限制: 不同的数据类型之间绝对不能直接进行计算,必须先进行强制类型转换。
- 没有三元运算符: Go 语言不支持
条件 ? 真值 : 假值这种写法。为了代码的可读性,官方强制要求您老老实实写完整的if...else。 - 独立的自增/自减语句: * Go 里面**只有
a++和a--**,没有前置的++a。 a++和a--在 Go 中是语句(Statement),而不是表达式(Expression)。这意味着它们不能参与任何复杂的数学运算或赋值操作。- ❌ 错误写法:
b := a++ + a++(编译直接报错)。 - ✅ 正确写法:只能单独写一行
a++。
2. if 条件判断
Go 语言的 if 语句更加简洁:
- 省略小括号: 判断条件不需要被
()包裹。 - 支持初始化语句(非常常用): 可以在条件判断之前,先执行一条简短的声明语句(以分号
;隔开)。这种方式声明的变量,其作用域仅限于这个if块内部,可以有效避免变量污染。
1 | // 先声明变量 a 并赋值为 1,然后判断 a > 0 |
3. switch 分支结构
Go 语言对 switch 进行了非常强大的升级,修复了其他语言中容易出错的设计:
- 默认自动
break(无需手写): 只要匹配到一个case并执行完毕,就会自动跳出switch,不会像 C/Java 那样发生意外的“穿透”。 - 单
case支持多个值: 多个条件可以合并在同一行,用逗号隔开。
1 | switch day { |
- 无条件
switch(替代多重if...else if):switch后面可以什么都不写,把条件判断直接写在case后面。代码看起来会比一连串的if else清爽得多。 - 显式穿透 (
fallthrough): 如果你确实需要执行完当前case后,继续强行执行紧挨着的下一个case,需要明确使用fallthrough关键字。
1 | score := 90 |
- Type Switch(类型断言): 这是 Go 语言特有的高级用法。当使用空接口
interface{}(可以接收任何类型的数据)时,可以利用switch type来判断里面到底装的是什么类型。
1 | var x interface{} = 3.14 // 空接口可以存任何值 |
结构体 (Struct)
1. 定义结构体
使用 type 和 struct 关键字来定义一个结构体。
1 | type Student struct { |
⚠️ 核心规则:极其重要的可见性(访问权限)
- 首字母大写(如
Name,Student): 表示公开(Exported),可以被其他包(package)访问和调用。- 首字母小写(如
name,age): 表示私有(Unexported),只能在定义它的包内部使用,外部包无法访问。
2. 结构体的三种初始化方式
Go 语言提供了多种声明和初始化结构体实例的方法,以适应不同的场景:
方式一:先声明,后逐个赋值
最基础的用法。先声明一个结构体变量(此时内部字段会被自动赋予零值,如字符串为空 "",数字为 0),然后再给需要的字段赋值。
1 | var stu Student |
方式二:键值对初始化 (⭐ 极力推荐)
在创建实例时,明确指定字段名和对应的值。
- 优点: 代码可读性极高,且不需要严格按照结构体定义的顺序来写,即使只初始化部分字段也不会报错(未初始化的字段默认为零值)。
1 | var stu2 = Student{ |
方式三:按顺序赋值 (省略字段名)
直接传入对应的值来初始化。
- 缺点: 必须严格按照定义结构体时的字段顺序进行赋值,且必须为所有字段赋值,少一个或者顺序错一个都会导致编译报错。不推荐在拥有大量字段的结构体中使用。
1 | var stu3 = Student{ |
3. 访问结构体字段
无论使用哪种方式初始化,都可以使用 点号 (.) 操作符来访问或修改结构体内部的具体字段。
1 | fmt.Println(stu3.Name) // 访问 |
数组与切片
1. 数组 (Array)
数组是固定长度的,定义后长度不可改变。注意:[3]int 和 [4]int 是完全不同的类型。
1 | // 1. 标准声明并赋值 |
2. 切片基础与 make 函数
切片就是定义时不确定长度的数组。
- 长度(
len):决定了“可见范围”。当长度小于容量时,只会显示长度以内的元素。由make创建时,剩余未显示的部分默认值是 0。 - 容量(
cap):决定了底层数组的“物理极限”。超出长度的部分是不可见的。
1 | // 1. 直接定义切片 |
3. 切片扩容规则
- 小容量切片:当原切片容量小于
256时,新容量直接按 2倍 扩容。 - 大容量切片:当原切片容量大于等于
256时,平滑过渡(不再无脑翻倍,避免极大的内存浪费)。
1 | // 扩容测试 1 |
4. 切片截取与“扩容解绑”机制 (极易踩坑)
切片是引用类型,直接修改它的元素会改变原来的值(共享数据内存)。
截取公式:使用
arr[low:high]截取切片时(前取后不取):长度 (
len) =high - low容量 (
cap) =底层数组的原始总容量 - low(起始截取位置)解绑机制:当
append触发扩容时,Go 会在底层开辟全新内存。此时,切片与原数组彻底解绑,此后的任何修改互不影响。
1 | arr := [3]int{1, 2, 3} |
Map
Map 是一种无序的键值对(Key-Value)数据结构。在 Go 语言中,Map 是引用类型。
1. 声明与初始化 ⚠️
Map 声明后默认是 nil,必须分配内存(初始化)后才能赋值,否则会引发 panic 报错。
1 | // ❌ 错误示范:只声明不初始化,直接赋值会报错 |
2. 核心操作与 “comma ok” 惯用法
- 零值特性: 当访问一个不存在的 Key 时,Go 不会报错,而是会返回该值类型的默认零值(例如
int返回0,string返回"")。 - 探空判断: 为了区分“真值为 0”和“Key 不存在”,必须使用
value, ok := m["key"]的双变量接收法。 - 删除操作: 使用内置的
delete函数。
1 | m := map[string]int{"张三": 18} |
3. 引用类型特性 (共享内存)
Map 作为引用类型,当把一个 Map 赋值给另一个变量时,它们共享同一块底层内存。
1 | m := map[string]int{"张三": 18} |
4. 🚫 核心铁律:比较与 Key 的限制
Map 自身的比较限制
两个 Map 之间是绝对不可以相互比较的(不能使用 == 或 !=)。Map 只能和 nil 进行比较,用来判断是否已经初始化。
1 | m1 := make(map[int]string) |
Map Key 的严格限制
能够支持使用
==和!=运算符进行判断的数据类型,才可以作为 Map 的 Key。
- ✅ 允许作为 Key 的类型:
int,float,string,bool, 甚至定长的数组。 - ❌ 绝对不能作为 Key 的类型 (Go 语言三大不可比较引用类型):
- 切片 (
slice): 最容易踩坑,切片长度动态变化,底层无法比较。 - 字典 (
map): Map 本身不能作为另一个 Map 的 Key。 - 函数 (
func): 函数体无法进行等值比较。
循环控制 (for / for range)
Go 语言在循环结构上做了极简设计:它删除了 while 和 do-while,整个语言中只有 for 这一种循环关键字。
1. for 循环的三种基本形态
通过灵活组合条件,for 可以实现其他语言中所有循环的功能。
1 | // 1. 标准经典循环 (初始化; 条件判断; 后置操作) |
2. for range 遍历与“值拷贝”陷阱 ⚠️
for range 是遍历数组、切片、Map 和字符串的最强工具,但一定要小心它的底层机制。
🛑 核心铁律:
在for range循环中,所有的元素都是值传递(值拷贝)。
您在循环里拿到的value,仅仅是底层元素的一个副本。直接修改value绝对不会改变原数据!
错误修改 vs 正确修改
1 | s := []int{1, 2, 3, 4, 5} |
(💡 提示:如果不需要索引或值,可以使用空白标识符 _将其忽略,例如for _, value := range s)
3. 字符串 (String) 遍历的巨大差异
在 Go 中遍历字符串有两种方式,它们在处理包含中文(或特殊字符)的字符串时,表现完全不同:
- 常规
for循环(按byte字节遍历):
一个中文字符(UTF-8)通常占 3 个字节。如果用i < len(str)去遍历,会把汉字拆碎成一个个不可读的字节,打印出来是乱码。 for range循环(按rune字符遍历):
它会自动识别 Unicode 编码,把一个完整的汉字当做一个独立的字符(rune)来处理,非常智能!
1 | str := "hello 世界" |
4. 循环控制语句
break: 彻底打断并跳出当前这一层循环。continue: 跳过当前这一次循环中剩下的代码,直接进入下一次循环。
1 | for i := 0; i < 10; i++ { |
指针 (Pointer)
Go 语言保留了指针来提高程序的内存操作效率,但为了安全,它对指针进行了极其严格的阉割和限制,去掉了 C/C++ 中容易出错的复杂操作。
1. 指针的两大基础符号
在 Go 语言中,操作指针只需要记住两个基础符号:
&(取址符): 放在变量前,用来获取该变量在内存中的真实地址。*(解引用符): 放在指针变量前,用来根据内存地址取回里面存的具体数值。
1 | a := 10 |
2. ⚠️ 核心考点:Go 指针的三大严苛限制
限制一:极其严格的类型匹配
指针也分类型!指向 int 的指针绝对不能接收 float64 的地址。
1 | // var p *int |
限制二:禁止指针运算 (Pointer Arithmetic)
这是 Go 相比于 C 语言最核心的改动。为了内存安全,Go 语言绝对不允许直接对指针进行加减偏移操作。
1 | // a := 10 |
限制三:警惕空指针恐慌 (Nil Pointer Dereference)
声明了一个指针但没有给它分配实际的内存地址时,它的默认值是 nil。如果强行对一个 nil 指针使用 * 取值,会导致程序直接崩溃(Panic)。
1 | // var p *int // 此时 p 是 nil,里面什么地址都没有 |
3. 为什么切片和 Map 不需要用指针?
这是日常开发中最常见的一个最佳实践。
- 普通变量是值传递: 像
a := 10; b := a,修改b绝对不会影响a,因为发生了完整拷贝。如果想通过函数修改外部的普通变量,就必须传指针。 - 引用类型自带“指针基因”: 切片 (
slice)、字典 (map) 和通道 (channel) 在底层的结构体中,已经包含了指向实际数据的指针。当您传递或赋值它们时,底层的数据本身就是共享的。
1 | // 切片本身就是引用类型,没必要再写成 *[]int |
函数与参数传递
1. 函数定义与返回值形式
Go 语言的函数非常灵活,支持多返回值和返回值命名。
1 | // 1. 无返回值 |
2. 匿名函数 (Function Literals)
函数在 Go 中可以作为变量赋值并直接调用。
1 | x := func() { |
3. 核心机制:值传递 (Value Pass) ⚠️
🛑 结论:Go 语言里所有参数的传递【全部都是】值传递。
指针传递 (*int)
虽然是值传递,但拷贝的是地址。函数拿到地址副本后,依然能通过 * 找到原内存并改变其值。
1 | func change1(a *int) { |
切片的“引用”本质 (Slice)
疑问: 为什么切片没传指针,原数据 s[0] 还是被修改了?
真相: 切片底层是一个包含三个字段的结构体:指向底层数组的指针、长度 (len) 和容量 (cap)。
- 当切片作为参数传递时,Go 拷贝了这个结构体副本。
- 关键点: 结构体里的指针也被原封不动地拷贝了过去。
- 函数内部通过这个指针副本,操作的是同一个底层数组。
1 | // 这里也是值传递:s[0] 被修改是因为拷贝了指向底层数组的指针 |
4. 运行验证 (您的源码)
1 | func main() { |
语言面向对象:结构体与方法
Go 语言没有 class 关键字,它通过 结构体 (struct) 组织属性,通过 方法 (method) 组织行为。
1. 结构体定义 (属性)
结构体是不同类型数据的集合,类似于其他语言中的类属性。
1 | type Student struct { |
2. 方法 (行为)
方法就是在函数关键字 func 和函数名之间加上 接收者 (Receiver)。
接收者的两种类型
- 值接收者
(s Student)**:拷贝一份结构体副本。在方法内修改属性,原对象不会**改变。 - 指针接收者
(s \*Student)**:传递结构体的地址副本。在方法内修改属性,原对象会**同步改变。
1 | // 指针接收者:可以修改原结构体的值 |
3. 语法糖:自动解引用
在 Go 中,即使方法定义的是指针接收者,您也可以直接用 实例.方法() 调用,Go 会自动帮您处理取地址操作。
1 | stu := Student{Name: "zhangsan", Age: 20} |
4. 为内置类型添加方法
铁律: 不能直接给 int、string 等内置类型定义方法。
解决方案: 使用 type 关键字定义 类型别名。
1 | type MyInt int // 起别名 |
5. 核心总结:Go 的 OOP 哲学
| 特性 | 实现方式 |
|---|---|
| 类 (Class) | 结构体 (struct) |
| 封装 (Encapsulation) | 首字母大写(公开)/ 小写(私有) |
| 行为 (Behavior) | 方法 (method) |
| 继承 (Inheritance) | 结构体嵌套 (Composition) |
面向对象:组合、嵌套与冲突处理
Go 语言抛弃了传统的 extends 继承机制,转而采用 “组合 (Composition)”。通过在结构体中嵌入另一个结构体(匿名字段),实现属性和方法的“继承”效果。
1. 组合与匿名字段 (Embedding)
当结构体中只写类型而不写字段名时,它被称为 匿名字段。
1 | type Animal struct { |
2. 核心特性:自动提升与就近原则
🌟 属性/方法提升 (Promotion)
如果外部结构体(如 Dog)没有同名成员,可以直接通过 dog.Name 访问内部结构体的成员。Go 会自动将其重定向到 dog.Animal.Name。
🌟 遮蔽效应 (Shadowing / 重写)
如果内外存在同名成员,Go 遵循 “就近原则”:
- 优先访问外部定义的成员。
- 若要访问内部被遮蔽的成员,必须显式指定路径。
1 | dog := Dog{ |
3. 命名冲突与歧义 (Ambiguity) ⚠️
当一个结构体同时嵌套了两个拥有同名成员的结构体时,Go 无法判断“自动提升”该指向谁。此时,必须显式指定,否则编译报错。
1 | type A struct { X int } |
4. 知识点对比总结
| 概念 | Go 的实现方式 | 其他语言 (Java/C++) |
|---|---|---|
| 继承 | 结构体组合 (Composition) | extends / public A |
| 子类调用父类 | 显式指定内嵌结构体名 | super / base |
| 方法重写 | 外部定义同名方法 (遮蔽) | @Override |
| 多重继承冲突 | 强制显式路径指定 | 虚继承 / 接口实现 |
接口 (Interface) 与多态
在 Go 语言中,接口是实现多态的基石。它采用“非侵入性”设计:不需要显式声明 implements,只要类型拥有了接口要求的全部方法,就自动实现了该接口。
1. 接口的定义与基本实现
核心规则: 一个类型必须实现接口中定义的全部方法,才算实现了该接口。
1 | type Animal interface { |
2. 多态的本质:看菜吃饭
定义: 只要实现了接口的类型,就可以把该类型的值赋值给接口类型的变量。
行为: 给接口变量传的是哪个结构体,接口就自动调用那个结构体对应实现的方法。
1 | type Cat struct { Name string } |
3. 核心考点:接收者类型对接口实现的影响 ⚠️
方法的接收者类型直接决定了哪些值可以赋值给接口:
| 方法接收者类型 | 实现接口的类型 | 赋值给接口的方式 |
|---|---|---|
值类型接收者 (d Dog) |
Dog 和 *Dog |
animal = d 或 animal = &d |
指针类型接收者 (d *Dog) |
**仅 \*Dog** |
必须使用 animal = &d |
代码验证:
1 | type Dog struct {} |
4. 一个类型实现多个接口
Go 鼓励小接口组合。一个结构体可以同时满足多个接口,从而具备多种“身份”。
1 | type Reader interface { Read() } |
5. 核心总结复盘
隐式实现:解耦了接口定义与具体实现。
赋值铁律:
如果是值类型作为接收者,那么值类型和指针类型都实现了这个接口。
如果是指针类型作为接收者,那么只有指针类型实现了这个接口。
多态应用:接口变量存储的是“动态类型”和“动态值”,调用时动态绑定。
空接口与类型断言
1. 空接口 (Empty Interface)
在 Go 语言中,interface{} 被称为空接口。由于它没有定义任何方法,因此所有的数据类型都默认实现了空接口。
- 用途: 当你不确定函数会接收什么类型的参数时(类似于 Java 的
Object或 C 语言的void*),可以使用空接口。 - 特性: 它可以存储任何值,包括
int、string、bool甚至是自定义的结构体。
1 | var x interface{} |
2. 类型断言 (Type Assertion)
当你把一个值赋给空接口后,它就丢失了原来的具体类型信息。如果你想把它转回原来的类型,就需要使用类型断言。
语法格式:
value, ok := x.(T)
x:接口变量。T:你预期的目标类型。value:如果断言成功,返回转换后的值。ok:布尔值,代表断言是否成功(强烈建议使用这种方式,防止程序崩溃)。
3. 代码实例分析
安全类型检查函数
您写的 CheckInt 函数是处理接口数据的标准写法。
1 | func CheckInt(x interface{}) bool { |
运行结果
1 | func main() { |
4. 关键避坑指南 ⚠️
- 直接断言的风险: 如果直接使用
num := x.(int)而不接收ok,一旦x内部不是int,程序会直接 panic (崩溃)。 - 类型分支 (Type Switch): 如果你需要判断多种可能的类型,使用
switch x.(type)会比一堆if-else更优雅。
💡 总结复盘
- 空接口:是万能容器。
- 类型断言:是从容器里把东西“辨认”出来的过程。
- 安全第一:始终使用
v, ok := x.(T)这种形式来保证代码的健壮性。

