视频加载失败

Go语言学习第二天

2285 字
11 分钟
Go语言学习第二天
Go语言学习第二天

Go 入门笔记(二):map、流程控制、函数、init 与 defer#

这是第二阶段的 Go 学习记录,内容来自五个练习文件:map、if 和 switch、for 循环、函数、init 和 defer。所有代码都在 Go 1.27.2 下实际运行过,文中输出均为真实结果。

一、map:键值对集合#

定义与读取#

var nameMap map[int]string = map[int]string{
1: "mirai",
2: "kami",
3: "kale",
4: "",
}
fmt.Println(nameMap) // map[1:mirai 2:kami 3:kale 4:]

map 用 map[键类型]值类型 声明,字面量初始化时每个 键: 值 一对。读取用 map[key]:

fmt.Printf("%#v\n", nameMap[4]) // ""
fmt.Printf("%#v\n", nameMap[5]) // ""

这里暴露了一个重要问题:键 4 存在但值是空字符串,键 5 根本不存在,两次读取的结果却一模一样。 因为读取不存在的键时,map 返回的是值类型的零值(string 的零值就是 "")。光看返回值,根本分不清”键不存在”和”键存在但值恰好是零值”。

comma-ok:判断键是否真的存在#

Go 的解决办法是让读取返回第二个值:

// ok 为 false 表示 map 中没有这个键值对
value, ok := nameMap[5]
fmt.Println(value, ok) // 输出: (空) false

ok 是 bool,表示键是否存在。这个写法叫 comma-ok 模式,是 Go 里非常常见的惯用法。

修改与删除#

nameMap[1] = "mirai1" // 直接赋值即可修改(键不存在则会新增)
delete(nameMap, 4) // delete 删除指定键值对
fmt.Println(nameMap) // map[1:mirai1 2:kami 3:kale]

nil map 的坑#

这是本次踩到的一个真实的坑。下面这段代码运行时会直接 panic:

var name map[string]string // 只声明,没有初始化
name["1"] = "1" // panic: assignment to entry in nil map

实际运行结果:

panic: assignment to entry in nil map
exit status 2

原因很简单:var name map[string]string 只是声明,此时 map 是 nil,里面没有任何底层数据结构,无法写入。写入前必须先初始化,两种方式都可以:

var name = map[string]string{}
var name = make(map[string]string)

注意对比:读 nil map 不会 panic(返回零值),写 nil map 才会 panic。另外切片也有零值 nil,但 append 一个 nil 切片是合法的——两种类型的 nil 行为不一样,不要混。

二、if 与 switch#

if#

if !false {
}

基本用法和其他语言一致。一个小差异:Go 的 if 条件不需要括号,if !false 而不是 if (!false),写了括号反而会被 gofmt 去掉。

switch:默认自带 break#

Go 的 switch 和 C/Java 有一个关键区别:每个 case 执行完自动 break,不会像 C 那样”穿透”到下一个 case。想穿透要显式写 fallthrough:

switch {
case true:
fmt.Print()
fallthrough
case false:
fmt.Print()
default:
fmt.Print()
}

这段代码还有两个细节值得注意:

  1. switch 后面可以不跟表达式,等价于 switch true,此时每个 case 直接写布尔条件即可。
  2. fallthrough 不会判断下一个 case 的条件,而是无条件执行下一个 case 的代码体。上面 case false 的条件明明是 false,但因为上一个 case 写了 fallthrough,它的代码体照样执行。fallthrough 一次只下落一层,除非下一个 case 里也写 fallthrough(我验证过,会一层层继续下落)。

所以 fallthrough 的语义是”接着执行下一个 case 的代码”,而不是”继续匹配”,用的时候要小心。

三、for:Go 唯一的循环#

Go 没有 while 和 do-while,所有循环都是 for 的变体。

基本形式与求和#

var sum int
for i := 1; i <= 10; i++ {
sum += i
}
fmt.Println(sum) // 55

三段式写法和其他语言一样(同样没有括号)。

死循环的三种写法#

for i := 0; true; i++ { } // 三段式,条件恒真
for true { } // 省略前后两段
for { } // 全部省略,最简洁,使用比较多

三种完全等价,实际开发中最常用的是 for {},配合内部的 break 或 return 退出。

range 遍历切片和 map#

除了按下标遍历,Go 提供了更常用的 range:

var slice = []string{"mirai", "kami"}
// 方式一:传统下标遍历
for i := 0; i < len(slice); i++ {
fmt.Println(i, slice[i])
}
// 方式二:range 同时拿到下标和值
for index, value := range slice {
fmt.Println(index, value)
}

range 遍历 map 拿到的是键和值:

var nameMap = map[string]string{"name": "kami"}
for key, value := range nameMap {
fmt.Println(key, value) // name kami
}

注意:map 的遍历顺序是随机的,Go 故意不保证顺序,所以不要把程序逻辑建立在 map 的遍历顺序上。不需要的变量可以用 _ 丢弃,比如只要值:for _, v := range slice。

练习:九九乘法表#

双重循环 + \t 制表符对齐:

for i := 1; i < 10; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%d*%d=%d\t", i, j, i*j)
}
fmt.Println()
}

内层循环条件是 j <= i,保证每行只打印到当前行数。实际输出(节选):

1*1=1
2*1=2 2*2=4
3*1=3 3*2=6 3*3=9
...
9*1=9 9*2=18 ... 9*9=81

四、函数#

参数的各种写法#

func f1() {} // 没有返回值,可以省略 return
func f2(str string) {} // 一个参数
func f3(str, name string) {} // 多个同类型参数可以合并声明
func f4(ageList ...int) {} // 可变参数

...int 是可变参数,可以传任意多个 int,效果类似 JavaScript 的 rest 参数。在函数内部,ageList 实际上是一个 []int 切片。

返回值#

// 单个返回值
func r1() bool {
return false
}
// 多个返回值:括号列出类型
func r2() (bool, string) {
return false, ""
}
// 多个返回值:命名返回值
func r3() (flag bool, name string) {
return flag, name
}

Go 的函数可以返回多个值,这是和 C/Java 很大的不同(之前 fmt.Scan 返回 n, err 就是这个特性)。r3 这种给返回值命名的写法,好处是签名本身就是文档,而且函数体内可以直接给 flag、name 赋值后用裸 return 返回。

匿名函数与闭包#

Go 不允许在函数内部声明命名函数,但可以把匿名函数赋给变量:

var f = func() {
fmt.Println("func")
}
f()

闭包和 JS 一样:内部函数可以访问外部函数的变量。我练习的例子是”先等待、再返回一个求和函数”:

func awaitAdd(awaitTime int) func(...int) int {
time.Sleep(time.Duration(awaitTime * int(time.Second)))
return func(numList ...int) (sum int) {
for _, v := range numList {
sum += v
}
return sum
}
}
// 调用:先等 2 秒,再对 1,2,3 求和
sum := awaitAdd(2)(1, 2, 3) // 6

awaitAdd(2) 返回一个函数,紧接着用 (1, 2, 3) 调用它——第一次见这种连续调用的写法,拆开理解就清晰了。time.Duration(awaitTime * int(time.Second)) 是把 int 秒数转成 Duration 类型,time.Sleep 只接受 Duration。

指针:通过地址修改外部变量#

func setName(name *string) {
*name = "mirai1"
}
name := "mirai"
setName(&name)
fmt.Println(name) // mirai1

指针和 C 语言一致:*string 表示”指向 string 的指针”,&name 取变量地址,*name 解引用(读写指针指向的值)。函数参数默认是值传递,不改指针的话,setName 里改的只是副本,外面的 name 不会变。传指针才能改到原变量——输出从 mirai 变成 mirai1 验证了这一点。

五、init 与 defer#

init:比 main 更早执行#

func init() {
fmt.Println("1")
}
func init() {
fmt.Println("2")
}
func init() {
fmt.Println("3")
}

init 是特殊函数:不需要调用,在 main 函数之前自动执行,常用于包初始化。一个文件里可以写多个 init,按书写顺序执行。

defer:延迟执行,先进后出#

defer func() {
fmt.Println("d1")
}()
defer func() {
fmt.Println("d2")
}()

defer 把函数推迟到当前函数返回前执行。多个 defer 的顺序是先进后出(类似栈):先声明的 d1 后执行。

整个程序的实际输出验证了执行顺序:

1 <- init 按顺序执行
2
3
d2 <- defer 后进先出
d1

即:所有 init → main 主体 → defer 逆序。

易错点与补充汇总#

本次注释整体比较准确,需要补充和提醒的点:

位置说明
map读取不存在的键返回零值,与”值为空”无法区分,要用 comma-ok 判断
map只声明不初始化是 nil map,写入会 panic(读取不会);nil 切片可以 append,行为不同
switchfallthrough不判断下一个 case 的条件,直接执行其代码体,且一次只下落一层
formap 的 range 遍历顺序随机,不能依赖
函数函数内不能声明命名函数,要用匿名函数;参数默认值传递,改外部变量需传指针
文件名10.inti和defer.go 里的 “inti” 应为 “init”,笔误不影响学习,记录一下

总结#

这一阶段把 Go 的核心流程和函数体系过了一遍:map 的 comma-ok 和 nil map 陷阱、switch 的自动 break 与 fallthrough 的真实语义、for/range 的统一循环模型、函数的多返回值/可变参数/闭包/指针,以及 init 和 defer 的执行时机。最深的两个体会:一是 Go 喜欢”显式”——fallthrough、指针、错误处理都要求写明白,不给你隐式行为;二是零值概念贯穿始终,map 的零值读取、返回值的零值、nil map 的 panic,全都和”类型默认值是什么”有关。

后续值得探索:defer 在资源释放场景的实际用法(如关闭文件)、指针和值类型的选择原则、map 的底层实现。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Go语言学习第二天
https://firefly.cuteleaf.cn/posts/go语言学习第二天/
作者
MIRAI
发布于
2026-10-16
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
MIRAI
心之所向,素履以往
公告
欢迎来到我的博客!
分类
标签
最新动态
站点统计
文章
5
分类
1
标签
8
总字数
9,034
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录