Go 基础语法复习:从声明到并发
Go 基础语法复习:从声明到并发
这篇文章适合已经接触过 Go,但隔了一段时间需要快速找回手感的人。重点不是完整讲语言规范,而是把写代码时最容易忘、最容易踩坑的规则重新串起来。
我会按这条线复习:
- 变量声明:
var和:= - 常见数据结构:数组、slice、map、struct、指针
- 作用域、生命周期和闭包
- 流程控制:
range、switch、defer、panic/recover - 函数、方法、接口
- 并发:goroutine、channel、select、WaitGroup、context
变量声明:var 和 :=
Go 里最常见的变量声明有两种:
var a int // 声明,零值是 0
var b = 10 // 声明并推导类型
c := 20 // 短声明,只能在函数内使用
简单记:
var是完整声明,包级和函数内部都能用。:=是短声明,只能在函数内部用。:=左边至少要有一个新变量。
x := 1
x = 2 // 赋值
x, y := 3, 4 // 可以,y 是新变量
包级作用域不能用 :=:
var version = "1.0" // 可以
// version := "1.0" // 不可以
原因是包级只能放声明,比如 var、const、type、func。:= 虽然看起来像声明,但它属于函数体里的短变量声明语句。
零值
Go 的变量声明后一定有默认值,不会处在“未初始化的随机状态”。
| 类型 | 零值 |
|---|---|
int | 0 |
string | "" |
bool | false |
| 指针、slice、map、channel、function、interface | nil |
这也是 Go 代码里经常直接声明变量后再赋值的原因。
常见数据结构
数组长度固定,长度也是类型的一部分:
a := [3]int{1, 2, 3}
slice 更常用,因为可以动态追加:
s := []int{1, 2, 3}
s = append(s, 4)
len 和 cap 是 slice 的核心:
s := make([]int, 0, 10)
len = 0:当前没有元素。cap = 10:底层预留了 10 个位置。- 只有
len范围内的位置可访问。
对比:
a := make([]int, 3, 10)
fmt.Println(a) // [0 0 0]
这里 len = 3,所以有 3 个已初始化的零值元素。
map 要先初始化才能写:
m := make(map[string]int)
m["go"] = 100
v, ok := m["go"]
delete(m, "go")
ok 用来判断 key 是否存在。因为 key 不存在时,取到的是值类型的零值。
struct 用来组织一组相关字段:
type User struct {
Name string
Age int
}
u := User{Name: "Tom", Age: 20}
指针保存变量地址,可以修改原值:
x := 10
p := &x
*p = 20
作用域、生命周期和闭包
作用域说的是变量名在哪里可见:
if true {
x := 1
}
// x 在这里不能用
生命周期说的是变量值实际活多久。Go 里可以返回局部变量地址:
func newUser() *User {
u := User{Name: "Tom"}
return &u
}
这不会产生悬空指针。编译器会让 u 逃逸到堆上,由 GC 管理。
但生命周期仍然会影响写代码:
- 文件、连接、锁、timer、context 这类资源要主动释放或取消。
- goroutine 如果没有退出条件,会一直活着。
- 小 slice 如果引用了大数组,可能让大数组一直不能释放。
- 闭包会捕获变量,使变量比所在函数活得更久。
闭包例子:
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
counter 已经返回了,但 n 还活着。原因是返回的匿名函数捕获了 n。
range
遍历 slice 或 array:
for i, v := range nums {
fmt.Println(i, v)
}
如果要修改 slice 元素,用下标:
for i := range nums {
nums[i] *= 2
}
原因是 range 里的 v 通常是元素副本。
遍历 map:
for k, v := range m {
fmt.Println(k, v)
}
map 遍历顺序不固定,不要依赖顺序。
遍历 string:
for i, r := range "你好" {
fmt.Println(i, r)
}
这里 i 是字节下标,r 是 rune。
补充:Go 1.22 起,循环变量按每轮迭代重新创建,闭包捕获循环变量的老问题少了。但维护旧项目时,仍然要留意项目使用的 Go 版本。
switch
普通 switch:
switch day {
case "Mon":
fmt.Println("work")
case "Sat", "Sun":
fmt.Println("rest")
default:
fmt.Println("unknown")
}
Go 的 switch 默认不会自动贯穿到下一个 case,通常不需要手写 break。
无表达式 switch 适合替代长 if else:
switch {
case score >= 90:
fmt.Println("A")
case score >= 60:
fmt.Println("pass")
default:
fmt.Println("fail")
}
类型判断可以用 type switch:
switch v := x.(type) {
case int:
fmt.Println(v + 1)
case string:
fmt.Println(len(v))
default:
fmt.Println("unknown")
}
defer
defer 会在当前函数结束前执行,执行顺序是后进先出。
defer fmt.Println("first")
defer fmt.Println("second")
输出顺序:
second
first
最常见用途是释放资源:
f, err := os.Open("a.txt")
if err != nil {
return err
}
defer f.Close()
一个重要细节:defer 的参数会立刻求值。
i := 0
defer fmt.Println(i)
i++
// 打印 0
如果用闭包,读取的是函数结束时的变量值:
i := 0
defer func() {
fmt.Println(i)
}()
i++
// 打印 1
panic 和 recover
普通错误应该用 error,不要把 panic 当作常规错误处理。
return 0, errors.New("bad input")
panic 更适合表示当前流程无法继续:
panic("impossible state")
recover 只能在 defer 里生效:
func safeRun() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("boom")
}
要点:
panic会停止当前 goroutine 的正常执行。- 已注册的
defer仍然会执行。 recover只能恢复同一个 goroutine 里的 panic。
函数、方法和接口
普通函数:
func add(a, b int) int {
return a + b
}
Go 常用多返回值表达“结果 + 错误”:
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
方法是带接收者的函数:
type User struct {
Name string
}
func (u User) DisplayName() string {
return u.Name
}
func (u *User) Rename(name string) {
u.Name = name
}
简单记:
- 值接收者:拷贝一份。
- 指针接收者:能修改原对象,也能避免大结构体复制。
接口描述行为:
type Speaker interface {
Speak() string
}
类型只要有对应方法,就自动实现接口:
type Dog struct{}
func (Dog) Speak() string {
return "wang"
}
func Say(s Speaker) {
fmt.Println(s.Speak())
}
Go 的接口是隐式实现,不需要写 implements。
空接口现在常写成 any:
var x any
any 等价于 interface{},表示可以接收任意类型。但能写具体类型时,优先写具体类型。
并发基础
Go 并发的主线很清楚:
- goroutine 负责并发执行。
- channel 负责 goroutine 之间通信。
- context 负责取消和超时。
启动 goroutine:
go func() {
fmt.Println("run")
}()
注意:main 结束,程序直接退出,不会等待 goroutine。
channel 传值:
ch := make(chan int)
go func() {
ch <- 1
}()
v := <-ch
fmt.Println(v)
无缓冲 channel:
ch := make(chan int)
发送和接收必须同时准备好,否则阻塞。
有缓冲 channel:
ch := make(chan int, 2)
ch <- 1
ch <- 2
缓冲没满时,发送不阻塞;缓冲没空时,接收不阻塞。
关闭 channel:
close(ch)
原则:发送方关闭 channel,接收方不要关。不要向已关闭的 channel 发送数据,否则会 panic。
遍历 channel:
for v := range ch {
fmt.Println(v)
}
range ch 会一直读,直到 channel 被关闭。
select 等多个 channel 谁先就绪:
select {
case v := <-ch:
fmt.Println(v)
case <-ctx.Done():
return
}
等待多个 goroutine 结束,用 sync.WaitGroup:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i)
}
wg.Wait()
取消 goroutine,用 context:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
select {
case <-ctx.Done():
return
default:
// do work
}
}
}()
多个 goroutine 同时读写同一个变量会有 data race:
count := 0
go func() {
count++
}()
共享数据要加锁,或者改成通过 channel 传递数据。
var mu sync.Mutex
count := 0
mu.Lock()
count++
mu.Unlock()
并发部分可以先记这几句话:
- goroutine 要有退出条件。
- 发送方负责关闭 channel。
range ch等到 channel 关闭才结束。- 多 goroutine 等待结束用
WaitGroup。 - 取消和超时控制用
context。 - 共享数据要么加锁,要么通过 channel 传递。
复习清单
最后把重点压成一张清单:
var是完整声明,:=是函数内短声明。- 包级不能用
:=,因为包级只能放声明。 - Go 变量都有零值。
len是当前元素数,cap是预留容量。range的值通常是副本,修改 slice 要用下标。- map 遍历顺序不固定。
switch默认不贯穿。defer后进先出,参数立刻求值。- 普通错误用
error,不要滥用panic。 - 方法是带接收者的函数。
- 指针接收者能修改原对象。
- interface 是隐式实现的行为约束。
- goroutine 是并发执行单元。
- channel 用来在 goroutine 间传值。
- context 用来做取消和超时控制。

浙公网安备 33010602011771号