Go 语法、数组、切片与 Map
# Go 语法、数组、切片与 Map
掌握 Go 的变量、零值、控制流程、数组、切片和 Map,并理解切片为什么不等于动态数组本身。
# 先记住一句话
Go 变量具有编译期确定的类型和可用的零值;切片是描述一段数组区域的视图,多个切片可能共享同一底层数组。
# 变量、常量与零值
// package main 表示这是可执行程序;程序从 main 函数开始运行。
package main
// import 引入标准库 fmt,用于格式化并输出内容。
import "fmt"
func main() {
// var 只声明不赋值时,Go 会自动给变量设置该类型的零值。
var count int // int 的零值是 0。
var enabled bool // bool 的零值是 false。
var title string // string 的零值是空字符串。
limit := 10 // := 在函数内声明并根据右侧推断类型。
const timeout = 30 // 常量值在编译期确定,不能再次赋值。
// Println 按顺序输出所有参数,并在末尾自动换行。
fmt.Println(count, enabled, title, limit, timeout) // 输出:0 false 10 30(空字符串不显示内容)。
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# := 和 = 有什么区别
:= 叫作短变量声明,用于在函数内部声明变量并赋值,变量类型由右侧的值推断;= 只给已经声明的变量重新赋值。需要明确类型、声明零值变量,或者在函数外声明包级变量时,使用 var。
func userInfo() (string, int) {
age := 18 // := 声明新变量 age,并推断为 int。
age = 19 // = 修改已经存在的 age。
name, age := "小明", 20 // name 是新变量,因此可以用 :=;age 会被重新赋值。
// age := 21 // 编译错误:左侧没有新变量,应该改用 age = 21。
return name, age
}
2
3
4
5
6
7
8
使用 := 时,左侧至少要有一个当前作用域中的新变量;它不能在函数外使用。可以简单记成:var 负责正式声明,:= 负责函数内快速声明,= 负责修改已有变量。
Go 不允许声明后完全未使用的局部变量,也不允许导入未使用的包。这些约束让重构后的死代码更早暴露。
前端转 Go:没有 `===` 和 `!==`
Go 只使用 == 判断相等、使用 != 判断不相等。Go 是静态类型语言,不会像 JavaScript 的 == 那样在比较前自动转换类型,因此不需要再提供一组 “严格相等” 运算符。
part != "" // part 不是空字符串。
len(parts) == 0 // parts 中没有元素。
// 1 === 1 // 编译错误:Go 没有 === 运算符。
// 1 == "1" // 编译错误:int 和 string 类型不兼容,不能直接比较。
// 0 == false // 编译错误:int 和 bool 类型不兼容,不能直接比较。
2
3
4
5
6
不同类型确实需要比较时,应先显式转换成目标类型,例如使用 strconv.Atoi 把字符串转成整数。还要注意:单个 = 表示赋值,两个 == 才表示相等比较。
# 控制流程
func classify(score int) string {
// score 是调用方传入的整数分数,函数最终返回对应的文字等级。
// Go 的 if 不写圆括号,但代码块的大括号不能省略。
if score >= 90 {
return "优秀" // return 会立即结束函数,后面的判断不再执行。
}
if score >= 60 {
return "通过"
}
return "需要复习" // 前两个条件都不满足时执行默认结果。
}
// sum 接收整数切片并返回所有元素之和。
func sum(values []int) int {
total := 0 // 累加器从 0 开始。
for _, value := range values {
// 下划线表示明确忽略本轮索引。
total += value // 等价于 total = total + value。
}
return total // 遍历结束后把累计结果返回给调用方。
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Go 只有 for 一种循环关键字,可以表达计数循环、条件循环和遍历。switch 默认匹配后自动结束,不需要为普通分支写 break。
# 数组和切片不是一回事
package main
import "fmt" // 用于把数组和切片内容输出到终端,观察共享底层数组的效果。
func main() {
// [3]string 表示长度固定为 3 的字符串数组;长度也是数组类型的一部分。
array := [3]string{"Python", "Go", "TypeScript"}
// array[:2] 是创建切片的语法,等价于 array[0:2]。
// 它包含下标 0、1 的元素,即 Python 和 Go;不复制数组中的元素。
firstTwo := array[:2]
alias := firstTwo // 只复制指针、长度和容量等切片描述,底层字符串数据没有复制。
alias[0] = "Rust"
fmt.Println(array) // [Rust Go TypeScript],因为它们共享底层数组。
// append 容量不足时会分配新数组,返回的切片必须重新接住。
firstTwo = append(firstTwo, "Java") // append 返回更新后的切片描述,因此必须重新赋值。
fmt.Println(firstTwo) // [Rust Go Java],容量足够,因此 append 复用底层数组并覆盖原来的 TypeScript。
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
slice 是 “切片” 这种数据结构的英文名称,不是方法。上面的 array[:2] 会创建一个切片变量 firstTwo,它内部保存的信息可以简化理解为(下图不是 Go 代码):
firstTwo(切片变量)
├─ pointer → 指向 array[0],即 "Python"
├─ len → 2,目前可以访问 Python 和 Go
└─ cap → 3,从 array[0] 到底层数组末端共有 3 个位置
2
3
4
因此,切片本身不保存这三个字符串,只记录 “从哪里开始、当前能访问几个元素、最多能扩展到几个元素”。这里 len 是 2、cap 是 3,所以追加一个元素时可以继续使用原来的底层数组。
如果函数要保留调用方传入的切片,而调用方之后还可能修改它,应根据所有权约定决定是否复制。
func cloneStrings(values []string) []string {
// values 是调用方传入的字符串切片;返回值也是字符串切片,但拥有独立的底层数组。
// nil 表示当前没有关联的实际数据,常作为切片、Map、指针、Channel、函数和接口的零值,
// 也常用于表示 “尚未初始化” 或 “没有结果”,例如 err == nil 表示没有错误。
// 这里的 []string(nil) 是 nil 转成的字符串切片,没有底层数组,长度和容量都是 0。
// values... 会把 values 的所有元素追加进去;有元素时会创建独立的底层数组,避免后续修改互相影响。
return append([]string(nil), values...)
}
2
3
4
5
6
7
8
Channel(通道)是 Go 中用于在 Goroutine(轻量级并发任务)之间传递数据、协调执行的通信工具,可以把它简单理解为一条安全传递数据的管道。nil Channel 表示这条通道还没有通过 make 创建,向它发送或从它接收数据都会一直阻塞。
nil 的具体用法取决于类型:可以对 nil 切片使用 len、cap、range 和 append,但按下标取元素会越界;可以读取 nil Map,但不能向其中写入数据;也不能解引用 nil 指针。
# Map 的读取与初始化
// package main 表示该文件属于可执行程序,fmt 用于打印读取结果。
package main
import "fmt"
func main() {
// map[键类型]值类型 是 Map 类型;下面使用字面量创建并初始化数据。
scores := map[string]int{
"python": 90,
"go": 95,
}
// 第二个返回值 ok 表示键是否存在,避免把不存在误判为分数 0。
value, ok := scores["rust"]
// ! 对布尔值取反,因此 !ok 表示键不存在。
if !ok {
fmt.Println("键不存在") // 当前 Map 没有 rust,因此执行这个分支。
} else {
fmt.Println(value) // 只有 Key 存在时,value 才是该 Key 对应的真实分数。
}
// delete 从 Map 删除指定键;键不存在时也不会报错。
delete(scores, "python")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
读取不存在的键会返回值类型的零值,因此需要用第二个布尔返回值区分 “键不存在” 和 “键存在但值刚好为零”。未初始化的 nil Map 可以读取,但写入会触发 panic,也就是当前代码无法继续正常执行;如果没有被 recover 恢复,程序最终会退出。写入前应使用字面量或 make 初始化,具体机制见 defer、panic 与 recover。
Mutex 和 Race Detector 与 Map 有什么关系
普通 Map 不支持无保护的并发读写。sync.Mutex 是互斥锁:访问共享 Map 前加锁、完成后解锁,保证同一时间只有一个 Goroutine 能进入这段读写代码。所有访问方必须使用同一把锁,只保护写操作却不保护并发读取,仍可能发生数据竞争。
Race Detector 是 Go 自带的数据竞争检测工具,通过 go test -race ./... 检查本次测试实际执行到的路径。它只能发现问题,不能替 Map 加锁,也不能证明未执行的路径一定安全。详细原理见 Mutex 保护的是不变量 和 Race Detector 只能发现跑到的竞争。
# 高频面试题与回答
1. 数组和切片有什么区别?参考答案
数组类型包含固定长度,赋值时会复制整个数组;切片是对底层数组一段区域的描述,包含指针、长度和容量。多个切片可能共享数据,append 也可能因为扩容切换到底层新数组。
2. Go 的零值有什么意义?参考答案
变量声明后即使没有显式赋值,也会获得类型对应的零值。这让很多类型可以直接使用并减少初始化代码,但切片、Map、指针和接口的零值涉及 nil,仍要了解哪些操作安全。
3. make 和 new 有什么区别?参考答案
new(T) 为类型 T 分配零值空间并返回 *T;make 只用于 Slice、Map 和 Channel,负责初始化它们内部可用的数据结构并直接返回该类型。例如,make(map[string]int) 创建的 Map 可以直接写入;new(map[string]int) 只会返回一个指针,指向的 Map 仍是 nil,不能直接写入数据。
4. append 一定会修改原切片吗?参考答案
不一定。容量足够时,append 可能复用并修改原底层数组;容量不足时会分配新数组并返回新的切片描述。因此必须接收返回值,并注意多个切片共享底层数组时可能相互影响。
5. nil Slice 和空 Slice 有什么区别?参考答案
两者长度都为 0,也都可以安全地遍历和 append,但 nil Slice 等于 nil,而已初始化的空 Slice 不等于 nil。它们在 JSON 等序列化边界上还可能分别表现为 null 和空数组,接口契约要求返回空数组时应显式初始化。
6. Go 的 Map 可以并发读写吗?参考答案
普通 Map 不能在没有同步保护时并发读写,否则可能发生数据竞争,严重时还会导致运行时错误。项目中通常使用同一把 Mutex 保护 Map 的所有读写操作。Race Detector 可以通过 go test -race 帮助检查数据竞争,但它只能发现测试实际执行到的问题,不能代替加锁。在读多写少等特定场景下,也可以考虑使用并发安全的 sync.Map。
# 接下来学什么
下一篇学习 Go 字符串、byte、rune 与 UTF-8,避免在中文长度、遍历和截断中混淆字节与字符。