Summary

Day01 是 2025 年 12 月 19 日写的,这篇是 2026 年 10 月 17 日。中间隔了十个月,跟"第二天"三个字已经没什么关系了。 所以标题里的 Day02 改成日期,不然越看越像自己骗自己。 翻出 A Tour of Go,接着上次的进度往下走。

For

Go 只有一种循环结构,即 for 循环。

最基本的 for 循环由三个部分组成,这三个部分之间使用分号分隔:

初始化语句(init statement):在第一次循环迭代之前执行。 条件表达式(condition expression):在每次循环迭代开始前进行判断。 后置语句(post statement):在每次循环迭代结束时执行。

初始化语句通常是一个简短的变量声明,在这里声明的变量只在该 for 语句的作用域内可见。

当布尔条件表达式的结果为 false 时,循环将停止执行。 注意:与 C、Java 或 JavaScript 等语言不同,Go 的 for 语句三个组成部分外部没有括号,同时代码块的大括号 { } 是必须存在的。

三个部分也不是必须都写。只留条件就是别的语言的 while,一个都不留就是死循环:

go
1
2
3
4
5
6
7
8
9
sum := 0
for i := 0; i < 5; i++ {   // 常规三件套
	sum += i
}

n := 1
for n < 100 {              // 这就是 while
	n *= 2
}

我一开始还去找 Go 里 while 的关键字,翻了半天,原来根本没有这个东西。

If

if 和 for 一样,条件外面不要括号,但是 {} 必须有。它有个别的语言没有的用法:条件前面可以先跑一个短语句,在那里声明的变量只活在 if/else 里面。

go
1
2
3
if v := 128; v%2 == 0 {
	fmt.Println(v, "是偶数")
}

看着别扭,用两次就顺手了。先取再判断的场景不用在外面多留一个变量,省事。

Switch

switch 不需要写 break,匹配到就停。它甚至可以不写条件,那就等价于一串 if-else:

go
1
2
3
4
5
6
7
8
9
os := "linux"
switch os {
case "darwin":
	fmt.Println("macOS")
case "linux":
	fmt.Println("Linux")
default:
	fmt.Println("其他")
}

switch 也支持那个前置短语句,switch n := 128; n { ... } 这样写,少一个变量名。

Defer

defer 是把这一句推迟到函数返回之前才执行,多个 defer 是后进先出,也就是栈的顺序。

go
1
2
3
4
func main() {
	defer fmt.Println("我最后才打印")
	fmt.Println("我先打印")
}

实际用的时候基本都是成对出现的:打开文件就 defer 关闭,加锁就 defer 解锁。写的时候不用记什么时候关,放在打开旁边就行,不会漏。

把这几段凑成一个 main 跑一下:

终端里跑 for、switch 和 defer 的输出

指针

Go 有指针,但没有指针运算。

go
1
2
3
v := Vertex{1, 2}
p := &v      // & 取地址
p.X = 10     // 访问指针的字段不用写 (*p).X,Go 自己解引用

那句 p.X 是这里让我觉得最舒服的地方。C 里得写 (*p).X 或者 p->X,Go 直接帮你把箭头省略了。

数组和切片

[5]int 是数组,长度属于类型的一部分,是值类型——赋值一次就是整体复制一份。

[]int 是切片,日常写的几乎都是它。它背后是一个数组,自己带长度和容量:

go
1
2
3
s := []int{2, 3, 5, 7, 11}
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
s = append(s, 13)

这里有个坑我特意试了一下:切出来的那一段和原切片共享同一块底层数组,改子切片会把原切片一起改掉。

go
1
2
3
sub := s[1:3]
sub[0] = 99
fmt.Println(s)   // [2 99 5 7 11 13]

不是复制,是真共享。官方有一篇专门讲这个的 Slices: usage and internals,我只看了开头,感觉得单独找时间读一遍,现在只能说"知道有这么回事"。

Map

map 就是别的语言的哈希表/字典,写法是这样的:

go
1
2
3
4
5
6
7
m := map[string]int{"go": 2009}
m["python"] = 1991
delete(m, "go")

if year, ok := m["python"]; ok {
	fmt.Println("map 取值:", year, ok)
}

取一个不存在的键不会报错也不会 panic,返回零值和 false。所以判断"有没有"要用两个返回值这种写法。这个设计比抛异常舒服得多,代价是得记着去判断 ok。

再来一遍,看看结果:

结构体、切片和 map 的运行结果

函数值和闭包

Go 里函数是一等公民,可以当变量传,也可以当返回值:

go
1
2
3
4
5
6
7
func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

闭包会牢牢记住外层那个变量,不是复制一份带走。所以每次调用返回的那个函数,累加的其实是同一个 sum。

先到这里

再往下就是方法和接口了,后面还有 goroutine 和 channel。那部分是 Go 真正有意思的地方,也是真正容易写错的地方。

不过今天先这样,A Tour of Go 前面这几节过了一遍,代码也都跑通了。等方法接口那节真用到的时候再回来补,反正笔记是给自己看的,不用一次写完。