GO学习笔记 | 第二章节 GO语言基础| 方法(函数)&& 控制结构 && 内置类型
GO学习笔记 | 第二章节 GO语言基础| 方法(函数)&& 控制结构 && 内置类型
核心内容:方法声明、函数式编程、控制结构(if/for/switch) 、内置类型
前置知识:变量声明、常量声明(iota)、包管理
一、两个注意点
1.1 GOPATH 目录结构
1 | GOPATH/ |
注意事项:
- 项目代码放在
src目录下 - 如果不在
src目录下,需要显式指定模块名 - 依赖缓存问题:可以删除整个
pkg/mod目录重新拉取
1.2 iota 回顾
1 | const ( |
建议:基本用法掌握即可,复杂用法了解就行。
二、方法声明

2.1 方法签名
1 | func 方法名(参数列表) 返回值 { |
组成部分:
func关键字- 方法名(大写 = 包外可访问,小写 = 包内私有)
- 参数列表
- 返回值
- 方法体
2.2 参数声明
基础写法
1 | // 无参数 |
Go 不支持函数重载
1 | // ❌ 编译错误!Go 不支持函数重载 |
替代方案:
- 使用不同的函数名:
AddInt,AddFloat,AddThree - 使用泛型(Go 1.18+)
- 使用可变参数
2.3 返回值

不带名字的返回值
1 | // 无返回值 |
带名字的返回值
name,age就是返回值的名字,你返回的时候必须返回两个值
1 | // 带名字的返回值 |
注意事项:
- 要么都带名字,要么都不带名字
- 带名字会扩大作用域(从方法开始到这个函数的结束)
- 个人习惯:不带名字,避免作用域污染
接收返回值

1 | //正常情况下 |
2.4 递归

1 | func Recursive(n int) { |
⚠️ 重要警告:
- 必须有退出机制,否则会导致 stack overflow
- 生产环境常见错误:A 调 B,B 调 C,C 调 A(循环递归)
- Go 的 goroutine 栈默认只有 2KB,很容易溢出
stack overflow 解决:
- 面试时:优化代码,找到递归退出条件
- 不要试图通过调大栈大小来解决(治标不治本)
三、函数式编程
3.1 函数是第一等公民
含义:函数可以像变量一样使用。
用法1:函数赋值给变量

1 | func Hello() { |
⚠️ 常见错误:
1 | myFunc := Hello() // ❌ 这是调用函数,把返回值赋给 myFunc |
用法2:带参数的函数赋值
1 | func Add(a, b int) int { |
用法3:局部方法(匿名函数)

1 | func Outer() { |
注:
1.fn和inner之前都没有声明过
2.后面的函数不写函数名
使用场景:
- 方法很长,想抽出一部分逻辑
- 但不想让别人使用(连同一个包的人都不想给用)
- 非常罕见的用法
3.2 方法作为返回值

1 | //意思是我返回一个返回值为string的func函数 |
Option 模式:
看不懂先跳过,不要紧的都,后面学的会了再回头来看就是了
1 | // 返回一个配置函数 |
3.3 闭包(Closure)

定义:方法 + 绑定的运行上下文
1 | func Closure(name string) func() string { |

关键点:
- 返回的函数仍可使用外部变量
- 外部变量不会被销毁,直到返回的函数用完
- 垃圾回收会自动管理
闭包修改外部变量
理解起来就是c可以看做一个对象一样的东西,count就是它的一个成员变量,然后它负责它的++。当然底层实现是什么样子并不清楚,只是可以这么理解一下
1 | func Counter() func() int { |
3.4 匿名函数立即调用

1 | func main() { |
这个result在这里是作为函数的返回值,是string类型的。而不是func类型的函数,所以下面才可以直接打印出来
常见用法:defer
1 | defer func() { |
四、不定参数
类似于C++的args包参数(可变参模板编程部分),详情可以参见侯捷c++11标准课笔记

4.1 基本用法
1 | func PrintNames(names ...string) { |
语法:...类型 表示不定参数,必须是最后一个参数。
4.2 传递切片
1 | func PrintNames(names ...string) { |
⚠️ 常见错误:
1 | PrintNames(names) // ❌ 编译错误 |
4.3 不定参数的本质
不定参数在方法内部就是一个切片(暂且理解为数组):
1 | func PrintNames(names ...string) { |
五、defer(延迟调用)

5.1 基本用法
1 | func main() { |
作用:在方法返回前一刻执行。
5.2 执行顺序:后进先出(LIFO)

1 | func main() { |
类比:栈(Stack)结构。
5.3 defer 与闭包

输出为1

图中第一段代码输出为1,第二段代码输出为0
可以这么理解,第一段代码是延迟调用而且没有参数,用的i的局部变量等他执行的时候i已经被改了
而第二段代码延迟调用但是运行到这段代码的时候已经把i=0传递进去了,变成func的局部变量了(或者说把i=0复制给了val),和i没关系了,所以i后面不管怎么改都无所谓了
情况1:defer 直接跟函数调用
1 | func main() { |
解释:defer 会立即计算参数值(0),延迟的是函数调用。
情况2:defer 跟闭包
1 | func main() { |
解释:闭包在真正执行时才取值,此时 i 已经是 1。
情况3:defer 传参给闭包
1 | func main() { |
解释:参数 i 立即计算(0),延迟的是闭包调用。
5.4 defer 修改返回值(带名字的返回值)

1 | // ❌ 无法修改 |
原理:
- 带名字的返回值:返回值在方法开始时就有固定位置,defer 修改的是这个位置
- 不带名字的返回值:return 时复制值到返回位置,defer 修改的是局部变量
5.5 defer 常见用途
1. 资源释放
1 | func Query() { |
2. 解锁
1 | func DoSomething() { |
3. 计时
1 | func SlowFunc() { |

第一个全是10,并且i的地址都是同一个。其实就是每一次都是同一个i,然后最后加完了才执行循环里面的func执行了10遍,打出来了10个10
第二个是9,8,7,6,5,4,3,2,1,0,并且val的地址也一直在变。而第二个每次都把i=1,i=2之类的直接传给val了,每个func都不一样,都有自己的val,然后最后倒着输出出来就是9,8,7,…了
第三个是9,8,7,6,5,4,3,2,1,0,并且j的地址也一直在变。j每一轮循环都是一个新的变量,所以地址也会不同,所以结果和第二个一样

六、控制结构
6.1 if-else

基本用法
1 | if age >= 18 { |
if-else
1 | if age >= 18 { |
if-else if-else
1 | if age >= 60 { |
注意:
- 条件成立进入分支后,不会继续判断其他分支
- 编译器不会检查条件是否有重叠
if 中定义变量
1 | // 在 if 中定义变量,作用域只在 if-else 块内 |
常见写法对比:
1 | // 写法1:if 中定义(推荐) |
6.2 for 循环


经典 for 循环
1 | for i := 0; i < 10; i++ { |
省略部分
1 | // 省略初始化和后置语句(类似 while) |
for range 遍历

遍历数组/切片:
1 | arr := []int{10, 20, 30} |
遍历 map:
1 | m := map[string]int{ |
⚠️ 重要警告:map 遍历顺序是随机的!
1 | m := map[string]int{"a": 1, "b": 2, "c": 3} |
不要依赖 map 的遍历顺序!
break 和 continue

1 | // break:跳出循环 |
⚠️ for-range 天坑:迭代变量地址相同

1 | users := []User{ |
正确做法:
1 | for i := range users { |
原则:不要对 for-range 的迭代变量(u)取地址!
6.3 switch

基本用法
1 | switch status { |
注意:
- 不需要写
break,Go 自动 break - 如果想继续执行下一个 case,用
fallthrough(很少用)
省略 switch 表达式
1 | switch { |
区别:
switch value:判断 value 等于哪个 caseswitch:判断哪个 case 条件为 true
switch 中定义变量
1 | switch status := getStatus(); status { |
七、方法调用总结
| 特性 | 说明 |
|---|---|
| 多返回值 | Go 支持方法返回多个值,这是与其他语言的重要区别 |
| 作用域控制 | 首字母大写 = 包外可访问,小写 = 包内私有 |
| 带名字的返回值 | 可以通过名字让返回值含义更清晰 |
| 函数是第一等公民 | 支持函数式编程,初学能看懂即可 |
| 闭包 | 方法 + 绑定的运行上下文 |
| defer | 后进先出(LIFO),在方法返回前执行 |
八、控制结构总结
| 结构 | 特点 |
|---|---|
| if-else | 可以在 if 中定义变量,作用域只在 if-else 块内 |
| for | 经典 for、while 形式、死循环、for range |
| for range | 遍历数组、切片、map;map 遍历顺序随机 |
| switch | 不需要 break,可以省略 switch 表达式 |
九、内置类型:数组(Array)

9.1 数组声明与初始化
1 | // 声明一个长度为 3 的 int 数组 |
9.2 长度 vs 容量
Go 的数组同时有 len(长度)和 cap(容量)两个概念:
len:已经放了多少个元素cap:最多能放多少个元素
对于数组来说,len 和 cap 永远相等。
1 | a := [3]int{8, 7} |
9.3 数组的特性与限制
- 长度固定,不可改变:不能对数组使用
append - 下标访问有编译期检查:下标越界连编译都通不过
- 可以用 for range 遍历,和其他类型一样
1 | // 编译错误:下标越界 |
实际开发中绝大多数场景都用切片。基本不用数组。
十、内置类型:切片(Slice)
切片是 Go 独有的概念,可以理解为动态数组。



10.1 从数组到切片:删掉长度就行
1 | // 数组:有长度 |
就这么一个方括号里有没有数字的区别。
10.2 用 make 创建切片
1 | // make(类型, 长度, 容量) |
| 参数 | 含义 |
|---|---|
| 第 1 个参数 | 切片类型 |
| 第 2 个参数 | 长度(len),当前已有元素个数 |
| 第 3 个参数(可选) | 容量(cap),底层数组的大小 |
10.3 append:往切片追加元素
1 | //往往这样进行初始化,长度设置为0,容量不为0,然后要加什么再用append加 |
10.4 子切片以及核心难点:切片的底层数组共享


这是切片最重要、也是最容易踩坑的概念。
1 | s1 := make([]int, 3, 4) // [0 0 0], len=3, cap=4 |
理解:切片本身只是一个窗口,多个切片可以共享同一个底层数组。修改一个切片的内容,其他共享同一底层数组的切片也会受影响。
扩容切断共享
1 | s1 := make([]int, 3, 4) // [0 0 0], len=3, cap=4 |
十一、内置类型:Map

11.1 Map 的创建与基本操作
1 | // 用 make 创建 |
11.2 判断 key 是否存在
如果不存在而你又接收了,那就给你一个零值
1 | v, ok := m1["key"] |
11.3 遍历顺序

不要依赖 map 的遍历顺序! 每次 for range 遍历 map,顺序可能不同。
1 | m := map[string]int{"a": 1, "b": 2, "c": 3} |
11.4 关于 channel
Go 还有一个内置类型 channel,用于 goroutine 之间的通信。到后续并发章节再详细讲。
11.5 comparable概念

十二、陷阱
陷阱1:函数重载
1 | // ❌ Go 不支持函数重载 |
陷阱2:递归没有退出条件
1 | // ❌ stack overflow |
陷阱3:defer 参数立即计算
1 | i := 0 |
陷阱4:for-range 取地址
1 | for _, u := range users { |
陷阱5:map 遍历顺序
1 | // ❌ 不要依赖 map 的遍历顺序 |
陷阱6:数组不能 append
1 | // 编译错误 |
陷阱7:切片底层数组共享
1 | s1 := make([]int, 3, 4) |
陷阱8:map 遍历顺序随机
1 | m := map[string]int{"a": 1, "b": 2, "c": 3} |
陷阱9:map 取不存在的 key 返回零值
1 | m := map[string]int{"a": 1} |










