GO学习笔记 | 第二章节 GO语言基础| 方法(函数)&& 控制结构 && 内置类型

核心内容:方法声明、函数式编程、控制结构(if/for/switch) 、内置类型
前置知识:变量声明、常量声明(iota)、包管理


一、两个注意点

1.1 GOPATH 目录结构

1
2
3
4
GOPATH/
├── src/ ← 源代码放在这里
├── bin/ ← go install 安装的命令
└── pkg/ ← go get 下载的依赖包

注意事项

  • 项目代码放在 src 目录下
  • 如果不在 src 目录下,需要显式指定模块名
  • 依赖缓存问题:可以删除整个 pkg/mod 目录重新拉取

1.2 iota 回顾

1
2
3
4
5
6
7
8
9
10
11
12
const (
A = iota // 0
B // 1
C // 2
)

// 复杂用法(了解即可)
const (
Flag1 = 1 << iota // 1 (二进制: 001)
Flag2 // 2 (二进制: 010)
Flag3 // 4 (二进制: 100)
)

建议:基本用法掌握即可,复杂用法了解就行。


二、方法声明

image-20260531223354363

2.1 方法签名

1
2
3
func 方法名(参数列表) 返回值 {
// 方法体
}

组成部分

  1. func 关键字
  2. 方法名(大写 = 包外可访问,小写 = 包内私有)
  3. 参数列表
  4. 返回值
  5. 方法体

2.2 参数声明

基础写法

1
2
3
4
5
6
7
8
9
10
11
// 无参数
func Func1() {}

// 单个参数
func Func2(a int) {}

// 多个参数
func Func3(a int, b string, c bool) {}

// 同类型简写 连续声明
func Func4(a, b, c int) {} // 三个都是 int

Go 不支持函数重载

1
2
3
4
// ❌ 编译错误!Go 不支持函数重载
func Add(a, b int) int { return a + b }
func Add(a, b float64) float64 { return a + b } // 错误:重复定义
func Add(a, b, c int) int { return a + b + c } // 错误:重复定义

替代方案

  • 使用不同的函数名:AddInt, AddFloat, AddThree
  • 使用泛型(Go 1.18+)
  • 使用可变参数

2.3 返回值

image-20260601094416149

不带名字的返回值

1
2
3
4
5
6
7
8
9
10
11
12
// 无返回值
func Func1() {}

// 单个返回值
func Func2() int {
return 42
}

// 多个返回值
func Func3() (string, int) {
return "hello", 18
}

带名字的返回值

name,age就是返回值的名字,你返回的时候必须返回两个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 带名字的返回值
func Func4() (name string, age int) {
name = "大明"
age = 18
return // 裸 return,自动返回 name 和 age
}

// 等价于
func Func4() (name string, age int) {
return "大明", 18
}

//这个返回的就是对应类型的零值,string对应空字符串,int对应0
func Func5()(name string,age int){
return
}

注意事项

  • 要么都带名字,要么都不带名字
  • 带名字会扩大作用域(从方法开始到这个函数的结束)
  • 个人习惯:不带名字,避免作用域污染

接收返回值

image-20260601094810887

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//正常情况下
var name string = "daming"
var age int = 18
name,age =func3() //这是没有新变量,因为name和age已经声明了

// 全部接收 name1和age1都没有声明,得用:=,这两个是新变量
name1, age1 := Func3()

// 忽略某个返回值(用下划线)
name2, _ := Func3() // 只接收 name,忽略 age
_, age2 := Func3() // 只接收 age,忽略 name

// 一个都不接收
Func3()

// ❌ 错误::= 左边必须至少有一个新变量 name和age都是已经声明过的变量,不能用:=
name, age := Func3()
name, age := Func3() // 编译错误:no new variables

// ✅ 正确:至少有一个新变量
name3, age := Func3() // name3 是新变量

2.4 递归

image-20260601101237058

1
2
3
4
5
6
7
func Recursive(n int) {
if n > 10 {
return // 必须有退出机制!
}
println(n)
Recursive(n + 1)
}

⚠️ 重要警告

  • 必须有退出机制,否则会导致 stack overflow
  • 生产环境常见错误:A 调 B,B 调 C,C 调 A(循环递归)
  • Go 的 goroutine 栈默认只有 2KB,很容易溢出

stack overflow 解决

  • 面试时:优化代码,找到递归退出条件
  • 不要试图通过调大栈大小来解决(治标不治本)

三、函数式编程

3.1 函数是第一等公民

含义:函数可以像变量一样使用。

用法1:函数赋值给变量

image-20260601101320661

1
2
3
4
5
6
7
8
func Hello() {
println("hello")
}

func main() {
myFunc := Hello // 注意:没有括号!
myFunc() // 调用
}

⚠️ 常见错误

1
2
myFunc := Hello()   // ❌ 这是调用函数,把返回值赋给 myFunc
myFunc := Hello // ✅ 这是把函数本身赋给 myFunc

用法2:带参数的函数赋值

1
2
3
4
5
6
7
8
func Add(a, b int) int {
return a + b
}

func main() {
myAdd := Add
result := myAdd(1, 2) // 调用时传参数
}

用法3:局部方法(匿名函数)

image-20260601102205555

1
2
3
4
5
6
7
8
9
10
func Outer() {
// 定义局部方法
inner := func() {
println("inner function")
}

inner() // 调用
}

// inner() // ❌ 错误:inner 只在 Outer 内部可用

注:

1.fn和inner之前都没有声明过

2.后面的函数不写函数名

使用场景

  • 方法很长,想抽出一部分逻辑
  • 但不想让别人使用(连同一个包的人都不想给用)
  • 非常罕见的用法

3.2 方法作为返回值

image-20260601120406080

1
2
3
4
5
6
7
8
9
10
11
//意思是我返回一个返回值为string的func函数
func GetFunc() func() string {
return func() string {
return "hello"
}
}

func main() {
fn := GetFunc()
result := fn() // "hello"
}

Option 模式

看不懂先跳过,不要紧的都,后面学的会了再回头来看就是了

1
2
3
4
5
6
7
8
// 返回一个配置函数
type Option func(*Config)

func WithTimeout(timeout time.Duration) Option {
return func(c *Config) {
c.Timeout = timeout
}
}

3.3 闭包(Closure)

image-20260618152152091

定义:方法 + 绑定的运行上下文

1
2
3
4
5
6
7
8
9
10
11
func Closure(name string) func() string {
return func() string {
return "hello " + name // 使用了外部的 name
}
}

func main() {
fn := Closure("大明")
// fn 已经脱离了 Closure(即closure已经调用结束了),但仍要使用 name,其实也就是说明大明在fn的上下文,必须要等fn用完name以后才会回收
println(fn()) // "hello 大明"
}

image-20260618152717615

关键点

  • 返回的函数仍可使用外部变量
  • 外部变量不会被销毁,直到返回的函数用完
  • 垃圾回收会自动管理

闭包修改外部变量

理解起来就是c可以看做一个对象一样的东西,count就是它的一个成员变量,然后它负责它的++。当然底层实现是什么样子并不清楚,只是可以这么理解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func Counter() func() int {
count := 0
return func() int {
count++ // 修改外部变量
return count
}
}

func main() {
c := Counter()
println(c()) // 1
println(c()) // 2
println(c()) // 3

c2 := Counter()
println(c2()) // 1(新的闭包,新的 count)
}

3.4 匿名函数立即调用

image-20260618151842874

1
2
3
4
5
6
7
func main() {
result := func() string {
return "hello"
}() // 定义完立即调用

println(result) // "hello"
}

这个result在这里是作为函数的返回值,是string类型的。而不是func类型的函数,所以下面才可以直接打印出来

常见用法:defer

1
2
3
defer func() {
println("cleanup")
}() // 匿名函数立即调用,返回值给 defer

四、不定参数

类似于C++的args包参数(可变参模板编程部分),详情可以参见侯捷c++11标准课笔记

image-20260618153253509

4.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
func PrintNames(names ...string) {
for _, name := range names {
println(name)
}
}

func main() {
PrintNames() // 不传也可以
PrintNames("Alice") // 传一个
PrintNames("Alice", "Bob", "Charlie") // 传多个
}

语法...类型 表示不定参数,必须是最后一个参数。

4.2 传递切片

1
2
3
4
5
6
7
8
func PrintNames(names ...string) {
// ...
}

func main() {
names := []string{"Alice", "Bob", "Charlie"}
PrintNames(names...) // 注意:必须加 ...
}

⚠️ 常见错误

1
2
PrintNames(names)    // ❌ 编译错误
PrintNames(names...) // ✅ 正确

4.3 不定参数的本质

不定参数在方法内部就是一个切片(暂且理解为数组):

1
2
3
4
func PrintNames(names ...string) {
// names 的类型是 []string
fmt.Printf("%T\n", names) // []string
}

五、defer(延迟调用)

image-20260618155000749

5.1 基本用法

1
2
3
4
5
6
func main() {
defer println("world")
println("hello")
// 输出:hello
// world
}

作用:在方法返回前一刻执行。

5.2 执行顺序:后进先出(LIFO)

image-20260618155125969

1
2
3
4
5
6
7
8
9
10
func main() {
defer println("1")
defer println("2")
defer println("3")
println("hello")
// 输出:hello
// 3
// 2
// 1
}

类比:栈(Stack)结构。

5.3 defer 与闭包

image-20260618155425219

输出为1

image-20260618155401490

图中第一段代码输出为1,第二段代码输出为0

可以这么理解,第一段代码是延迟调用而且没有参数,用的i的局部变量等他执行的时候i已经被改了

而第二段代码延迟调用但是运行到这段代码的时候已经把i=0传递进去了,变成func的局部变量了(或者说把i=0复制给了val),和i没关系了,所以i后面不管怎么改都无所谓了

情况1:defer 直接跟函数调用

1
2
3
4
5
func main() {
i := 0
defer fmt.Println(i) // 输出:0
i++
}

解释defer 会立即计算参数值(0),延迟的是函数调用。

情况2:defer 跟闭包

1
2
3
4
5
6
7
func main() {
i := 0
defer func() {
fmt.Println(i) // 输出:1
}()
i++
}

解释:闭包在真正执行时才取值,此时 i 已经是 1。

情况3:defer 传参给闭包

1
2
3
4
5
6
7
func main() {
i := 0
defer func(v int) {
fmt.Println(v) // 输出:0
}(i)
i++
}

解释:参数 i 立即计算(0),延迟的是闭包调用。

5.4 defer 修改返回值(带名字的返回值)

image-20260618160442438

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 无法修改
func ReturnV1() int {
a := 0
defer func() {
a = 1 // 修改的是局部变量 a,不是返回值
}()
return a // 已经把 0 写入返回值,再改 a 没用
}

// ✅ 可以修改
func ReturnV2() (a int) { // 带名字的返回值
defer func() {
a = 1 // 修改的就是返回值本身
}()
return // 裸 return,返回 a
}

func main() {
println(ReturnV1()) // 0
println(ReturnV2()) // 1
}

原理

  • 带名字的返回值:返回值在方法开始时就有固定位置,defer 修改的是这个位置
  • 不带名字的返回值:return 时复制值到返回位置,defer 修改的是局部变量

5.5 defer 常见用途

1. 资源释放

1
2
3
4
5
6
func Query() {
db := OpenDB()
defer db.Close() // 确保关闭

// 执行业务逻辑...
}

2. 解锁

1
2
3
4
5
6
func DoSomething() {
mu.Lock()
defer mu.Unlock()

// 执行业务逻辑...
}

3. 计时

1
2
3
4
5
6
7
func SlowFunc() {
defer func(start time.Time) {
println("耗时:", time.Since(start))
}(time.Now())

// 执行业务逻辑...
}

image-20260618162951824

第一个全是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每一轮循环都是一个新的变量,所以地址也会不同,所以结果和第二个一样

image-20260618163512794


六、控制结构

6.1 if-else

image-20260618163914667

基本用法

1
2
3
if age >= 18 {
println("成年了")
}

if-else

1
2
3
4
5
if age >= 18 {
println("成年了")
} else {
println("未成年")
}

if-else if-else

1
2
3
4
5
6
7
8
9
if age >= 60 {
println("老年")
} else if age >= 18 {
println("成年")
} else if age >= 12 {
println("青少年")
} else {
println("儿童")
}

注意

  • 条件成立进入分支后,不会继续判断其他分支
  • 编译器不会检查条件是否有重叠

if 中定义变量

1
2
3
4
5
6
7
8
9
// 在 if 中定义变量,作用域只在 if-else 块内
if distance := calcDistance(a, b); distance < 100 {
println("很近")
} else if distance < 1000 {
println("有点远")
} else {
println("很远")
}
// distance 在这里不可用

常见写法对比

1
2
3
4
5
6
7
8
9
10
// 写法1:if 中定义(推荐)
if err := doSomething(); err != nil {
return err
}

// 写法2:传统写法
err := doSomething()
if err != nil {
return err
}

6.2 for 循环

image-20260618165008349

image-20260618165305447

经典 for 循环

1
2
3
for i := 0; i < 10; i++ {
println(i)
}

省略部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 省略初始化和后置语句(类似 while)
i := 0
for i < 10 {
println(i)
i++
}

// 只有条件(类似 while)
for condition {
// ...
}

// 死循环
for {
// ...
}

for range 遍历

image-20260618165337194

遍历数组/切片

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
arr := []int{10, 20, 30}

// 获取下标和值
for index, value := range arr {
println(index, value)
}
// 输出:
// 0 10
// 1 20
// 2 30

//用下标访问也可以
for index := range arr {
println(index, arr[index])
}

// 只获取下标
for index := range arr {
println(index)
}

// 只获取值(用下划线忽略下标)
for _, value := range arr {
println(value)
}

遍历 map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
m := map[string]int{
"k1": 100,
"k2": 200,
}

// 获取 key 和 value
for k, v := range m {
println(k, v)
}

// 获取 key 和 value
for k := range m {
println(k, m[k])
}

// 只获取 key
for k := range m {
println(k)
}

⚠️ 重要警告:map 遍历顺序是随机的!

1
2
3
4
5
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k := range m {
println(k)
}
// 每次运行顺序可能不同!

不要依赖 map 的遍历顺序!

break 和 continue

image-20260618170638266

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// break:跳出循环
for i := 0; i < 100; i++ {
if i > 10 {
break // i > 10 时跳出循环
}
println(i)
}

// continue:跳过本次循环
for i := 0; i < 10; i++ {
if i%2 == 1 {
continue // 奇数跳过
}
println(i) // 只输出偶数
}

⚠️ for-range 天坑:迭代变量地址相同

image-20260618165956231

1
2
3
4
5
6
7
8
9
10
11
users := []User{
{Name: "Alice"},
{Name: "Bob"},
}

var ptrs []*User
for _, u := range users {
ptrs = append(ptrs, &u) // ❌ 错误!所有 ptrs 指向同一个地址
}

// 结果:ptrs[0] 和 ptrs[1] 指向同一个对象!

正确做法

1
2
3
for i := range users {
ptrs = append(ptrs, &users[i]) // ✅ 正确
}

原则:不要对 for-range 的迭代变量(u)取地址!

6.3 switch

image-20260618170757130

基本用法

1
2
3
4
5
6
7
8
9
10
switch status {
case 1:
println("初始化")
case 2:
println("运行中")
case 3:
println("已停止")
default:
println("未知状态")
}

注意

  • 不需要写 break,Go 自动 break
  • 如果想继续执行下一个 case,用 fallthrough(很少用)

省略 switch 表达式

1
2
3
4
5
6
7
8
9
10
switch {
case age >= 60:
println("老年")
case age >= 18:
println("成年")
case age >= 12:
println("青少年")
default:
println("儿童")
}

区别

  • switch value:判断 value 等于哪个 case
  • switch:判断哪个 case 条件为 true

switch 中定义变量

1
2
3
4
5
6
7
8
9
switch status := getStatus(); status {
case 1:
println("初始化")
case 2:
println("运行中")
default:
println("未知状态")
}
// status 在这里不可用

七、方法调用总结

特性 说明
多返回值 Go 支持方法返回多个值,这是与其他语言的重要区别
作用域控制 首字母大写 = 包外可访问,小写 = 包内私有
带名字的返回值 可以通过名字让返回值含义更清晰
函数是第一等公民 支持函数式编程,初学能看懂即可
闭包 方法 + 绑定的运行上下文
defer 后进先出(LIFO),在方法返回前执行

八、控制结构总结

结构 特点
if-else 可以在 if 中定义变量,作用域只在 if-else 块内
for 经典 for、while 形式、死循环、for range
for range 遍历数组、切片、map;map 遍历顺序随机
switch 不需要 break,可以省略 switch 表达式

九、内置类型:数组(Array)

image-20260618181602926

9.1 数组声明与初始化

1
2
3
4
5
6
7
8
9
10
11
// 声明一个长度为 3 的 int 数组
var a [3]int

// 直接初始化
a1 := [3]int{8, 7, 9}

// 只初始化部分元素,其余为零值
a2 := [3]int{8, 7} // [8, 7, 0]

// 一个都不初始化,全是零值
a3 := [3]int{} // [0, 0, 0]

9.2 长度 vs 容量

Go 的数组同时有 len(长度)和 cap(容量)两个概念:

  • len:已经放了多少个元素
  • cap:最多能放多少个元素

对于数组来说,lencap 永远相等。

1
2
3
a := [3]int{8, 7}
fmt.Println(len(a)) // 3
fmt.Println(cap(a)) // 3

9.3 数组的特性与限制

  • 长度固定,不可改变:不能对数组使用 append
  • 下标访问有编译期检查:下标越界连编译都通不过
  • 可以用 for range 遍历,和其他类型一样
1
2
3
// 编译错误:下标越界
a := [3]int{1, 2, 3}
fmt.Println(a[3]) // invalid argument: index 3 out of bounds

实际开发中绝大多数场景都用切片。基本不用数组。


十、内置类型:切片(Slice)

切片是 Go 独有的概念,可以理解为动态数组

image-20260618181950934

image-20260618182535255

image-20260618182614558

10.1 从数组到切片:删掉长度就行

1
2
3
4
5
// 数组:有长度
a := [3]int{1, 2, 3}

// 切片:没长度
s := []int{1, 2, 3}

就这么一个方括号里有没有数字的区别。

10.2 用 make 创建切片

1
2
3
4
5
// make(类型, 长度, 容量)
s1 := make([]int, 3, 4) // len=3, cap=4

// 只传一个参数:长度 = 容量
s2 := make([]int, 4) // len=4, cap=4
参数 含义
第 1 个参数 切片类型
第 2 个参数 长度(len),当前已有元素个数
第 3 个参数(可选) 容量(cap),底层数组的大小

10.3 append:往切片追加元素

1
2
3
4
5
6
7
8
9
10
11
12
//往往这样进行初始化,长度设置为0,容量不为0,然后要加什么再用append加
s := make([]int, 0, 3) // 空切片,容量 3
s = append(s, 1) // [1], len=1, cap=3
s = append(s, 2) // [1 2], len=2, cap=3
s = append(s, 3) // [1 2 3], len=3, cap=3
s = append(s, 4) // [1 2 3 4], len=4, cap=6(扩容了!)

//或者
s := make([]int, 3) // 空切片,容量 3
s[0]=1
s[2]=2
s[3]=4

10.4 子切片以及核心难点:切片的底层数组共享

image-20260618183358012

image-20260618183432735

这是切片最重要、也是最容易踩坑的概念。

1
2
3
4
5
s1 := make([]int, 3, 4)  // [0 0 0], len=3, cap=4
s2 := s1 // s2 和 s1 指向同一个底层数组!

s2[0] = 100
fmt.Println(s1[0]) // 100 —— s1 也被改了!

理解:切片本身只是一个窗口,多个切片可以共享同一个底层数组。修改一个切片的内容,其他共享同一底层数组的切片也会受影响。

扩容切断共享

1
2
3
4
5
6
7
8
9
s1 := make([]int, 3, 4)  // [0 0 0], len=3, cap=4
s2 := s1 // 共享底层数组

s2 = append(s2, 1) // cap 还够,追加到共享数组
fmt.Println(s1[0]) // 0(没被影响,因为追加的是第 4 个位置)

s2 = append(s2, 2) // cap 不够了,扩容,分配新数组
s2[0] = 999
fmt.Println(s1[0]) // 0 —— 扩容后已经不共享了

十一、内置类型:Map

image-20260618183901952

11.1 Map 的创建与基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 用 make 创建
m1 := make(map[string]int)

// 直接初始化
m2 := map[string]int{
"k1": 100,
"k2": 200,
}

// 赋值 也可以叫做插入
m1["key"] = 42

// 取值
v := m1["key"]

// 删除
delete(m1, "key")

// 容量
m2 := make(map[string]int,12)

11.2 判断 key 是否存在

如果不存在而你又接收了,那就给你一个零值

1
2
3
4
5
6
v, ok := m1["key"]
if ok {
fmt.Println("存在, 值为", v)
} else {
fmt.Println("不存在")
}

11.3 遍历顺序

image-20260618184442228

不要依赖 map 的遍历顺序! 每次 for range 遍历 map,顺序可能不同。

1
2
3
4
5
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k := range m {
println(k)
}
// 每次运行的输出顺序可能不一样!

11.4 关于 channel

Go 还有一个内置类型 channel,用于 goroutine 之间的通信。到后续并发章节再详细讲。

11.5 comparable概念

image-20260618184605131


十二、陷阱

陷阱1:函数重载

1
2
3
// ❌ Go 不支持函数重载
func Add(a, b int) int { return a + b }
func Add(a, b float64) float64 { return a + b } // 编译错误

陷阱2:递归没有退出条件

1
2
3
4
// ❌ stack overflow
func Recursive(n int) {
Recursive(n + 1) // 没有退出条件!
}

陷阱3:defer 参数立即计算

1
2
3
i := 0
defer fmt.Println(i) // 输出 0,不是 1
i++

陷阱4:for-range 取地址

1
2
3
for _, u := range users {
ptrs = append(ptrs, &u) // ❌ 所有 ptrs 指向同一个地址
}

陷阱5:map 遍历顺序

1
2
3
4
// ❌ 不要依赖 map 的遍历顺序
for k := range m {
// 顺序是随机的!
}

陷阱6:数组不能 append

1
2
3
// 编译错误
a := [3]int{1, 2, 3}
a = append(a, 4) // first argument to append must be slice

陷阱7:切片底层数组共享

1
2
3
4
s1 := make([]int, 3, 4)
s2 := s1
s2[0] = 100
fmt.Println(s1[0]) // 100 —— s1 被意外修改!

陷阱8:map 遍历顺序随机

1
2
3
4
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k := range m {
// 顺序每次都可能不同,不要依赖!
}

陷阱9:map 取不存在的 key 返回零值

1
2
3
4
m := map[string]int{"a": 1}
v := m["b"] // v = 0(int 的零值),不会报错
// 正确做法:用逗号 ok 模式
v, ok := m["b"]