01. 变量、类型与控制流
这一节先把 Go 最常见的语法元素过一遍。你不需要一次记住全部细节,但需要建立一种感觉:Go 倾向于简单、显式、可读。
本节目标
- 理解 Go 的变量声明方式和零值
- 熟悉常见基础类型、数组、切片、映射
- 掌握
if、for、switch这些控制流写法 - 能看懂项目里最基础的数据处理代码
变量声明
Go 里最常见的声明方式有三种:
// 显式指定类型
var projectName string = "commit-dashboard"
// 让编译器推断类型
var port = 8080
// 函数内部最常见的短变量声明
enabled := true
// 常量
const defaultPageSize = 10
经验上可以这么理解:
- 函数内优先使用
:= - 包级变量通常使用
var - 不会变化的值使用
const
基础类型与零值
Go 的变量在声明后即使没有显式赋值,也会自动拥有零值:
| 类型 | 示例 | 零值 |
|---|---|---|
string | "hello" | "" |
int / int64 | 42 | 0 |
bool | true | false |
float64 | 3.14 | 0 |
| 指针 | *Repo | nil |
| 切片 | []string | nil |
| map | map[string]int | nil |
示例:
var name string
var count int
var tags []string
fmt.Println(name == "") // true
fmt.Println(count == 0) // true
fmt.Println(tags == nil) // true
这也是为什么在 Go 里我们经常能通过“是否为零值”判断一个字段有没有被设置。
数组:固定长度的序列
Go 里确实有数组,但它和切片不是一回事。
var top3 [3]string
top3[0] = "api"
top3[1] = "web"
top3[2] = "worker"
fmt.Println(len(top3)) // 3
也可以直接初始化:
languages := [3]string{"Go", "TypeScript", "SQL"}
数组的特点是:
- 长度是类型的一部分,
[3]string和[4]string不是同一种类型 - 长度固定,创建后不能扩容
- 更适合少量、固定长度的数据
在日常 Web 项目里,数组没有切片常用,但理解它能帮助你看懂“为什么切片不是数组本身”。
切片:最常用的集合类型
切片可以理解成“动态数组”,在 Go 里非常常见。
repos := []string{"api", "web"}
repos = append(repos, "worker")
fmt.Println(len(repos)) // 3
fmt.Println(repos[0]) // api
也可以用 make 创建:
scores := make([]int, 3, 5)
scores[0] = 95
scores[1] = 88
scores[2] = 100
这里的 make([]int, 3, 5) 表示:
- 长度
len是3 - 容量
cap是5
切片还支持再次切片:
repos := []string{"api", "web", "worker", "docs"}
backend := repos[0:3]
fmt.Println(backend) // [api web worker]
配合 range 使用最顺手:
for index, repo := range repos {
fmt.Println(index, repo)
}
如果你不需要索引,可以用 _ 忽略:
for _, repo := range repos {
fmt.Println(repo)
}
你可以先用一句话记住数组和切片的区别:
- 数组是固定长度的数据本体
- 切片是对一段连续数据的轻量视图
所以工程代码里我们几乎总是在用切片,比如 []string、[]Repository、[]Commit。
map:键值对结构
Go 的 map 很适合做查表、聚合、计数:
commitCount := map[string]int{
"alice": 12,
"bob": 8,
}
commitCount["carol"] = 5
fmt.Println(commitCount["alice"])
判断键是否存在时,常见写法是:
count, ok := commitCount["david"]
if !ok {
fmt.Println("david 不存在")
}
fmt.Println(count)
if:支持前置语句
Go 的 if 经常和错误处理一起使用:
if err := doSomething(); err != nil {
return err
}
这种写法非常常见,你在后面的仓储层、处理器里会频繁看到。
for:Go 唯一的循环
Go 没有 while,统一使用 for:
for i := 0; i < 3; i++ {
fmt.Println(i)
}
也可以只写条件:
count := 0
for count < 3 {
count++
}
还可以写成“无限循环”:
for {
// 常用于 worker、消息循环、重试逻辑
}
switch:更整洁的分支判断
switch env := "dev"; env {
case "dev":
fmt.Println("开发环境")
case "prod":
fmt.Println("生产环境")
default:
fmt.Println("未知环境")
}
Go 的 switch 默认不会自动贯穿到下一个 case,所以通常更安全。
一个贴近项目的小例子
下面这个例子模拟“读取分页参数并修正默认值”的思路:
type RepoQuery struct {
Page int
PageSize int
Keywords string
}
func normalizeQuery(q RepoQuery) RepoQuery {
if q.Page < 1 {
q.Page = 1
}
if q.PageSize < 1 || q.PageSize > 100 {
q.PageSize = 10
}
switch q.Keywords {
case "":
fmt.Println("无搜索关键词")
default:
fmt.Println("按关键词过滤:", q.Keywords)
}
return q
}
这类“接收数据 -> 处理默认值 -> 分支判断”的逻辑,在 Web 后端里非常常见。
常见易错点
- 数组和切片不是同一种类型,不能直接混着用
nil的切片可以append,但nil的 map 不能直接赋值:=只能在函数内部使用for range拿到的是副本,修改元素时要注意是否真的改到了原值- Go 没有三元表达式,条件判断请老老实实写
if
小结
你现在需要记住的核心点只有三个:
- Go 喜欢显式和简单的写法。
- 零值是 Go 里非常重要的默认行为。
- 切片、map、
if err != nil、for range是最常见的代码形态,而数组更偏底层和固定长度场景。