Go语言学习第二天
- 1Go语言学习第四天
- 2Go语言学习第三天
- 3Go语言学习第二天本文
- 4Go语言学习第一天

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) // 输出: (空) falseok 是 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 mapexit 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() fallthroughcase false: fmt.Print()default: fmt.Print()}这段代码还有两个细节值得注意:
switch后面可以不跟表达式,等价于switch true,此时每个 case 直接写布尔条件即可。fallthrough不会判断下一个 case 的条件,而是无条件执行下一个 case 的代码体。上面case false的条件明明是 false,但因为上一个 case 写了 fallthrough,它的代码体照样执行。fallthrough 一次只下落一层,除非下一个 case 里也写 fallthrough(我验证过,会一层层继续下落)。
所以 fallthrough 的语义是”接着执行下一个 case 的代码”,而不是”继续匹配”,用的时候要小心。
三、for:Go 唯一的循环
Go 没有 while 和 do-while,所有循环都是 for 的变体。
基本形式与求和
var sum intfor 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=12*1=2 2*2=43*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) // 6awaitAdd(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 按顺序执行23d2 <- defer 后进先出d1即:所有 init → main 主体 → defer 逆序。
易错点与补充汇总
本次注释整体比较准确,需要补充和提醒的点:
| 位置 | 说明 |
|---|---|
| map | 读取不存在的键返回零值,与”值为空”无法区分,要用 comma-ok 判断 |
| map | 只声明不初始化是 nil map,写入会 panic(读取不会);nil 切片可以 append,行为不同 |
| switch | fallthrough不判断下一个 case 的条件,直接执行其代码体,且一次只下落一层 |
| for | map 的 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 的底层实现。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!




