Go 基础语法复习:从声明到并发

Go 基础语法复习:从声明到并发

这篇文章适合已经接触过 Go,但隔了一段时间需要快速找回手感的人。重点不是完整讲语言规范,而是把写代码时最容易忘、最容易踩坑的规则重新串起来。

我会按这条线复习:

  • 变量声明:var:=
  • 常见数据结构:数组、slice、map、struct、指针
  • 作用域、生命周期和闭包
  • 流程控制:rangeswitchdeferpanic/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" // 不可以

原因是包级只能放声明,比如 varconsttypefunc:= 虽然看起来像声明,但它属于函数体里的短变量声明语句。

零值

Go 的变量声明后一定有默认值,不会处在“未初始化的随机状态”。

类型零值
int0
string""
boolfalse
指针、slice、map、channel、function、interfacenil

这也是 Go 代码里经常直接声明变量后再赋值的原因。

常见数据结构

数组长度固定,长度也是类型的一部分:

a := [3]int{1, 2, 3}

slice 更常用,因为可以动态追加:

s := []int{1, 2, 3}
s = append(s, 4)

lencap 是 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

panicrecover

普通错误应该用 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 用来做取消和超时控制。

参考资料

posted @ 2026-07-06 14:23  牛马chen  阅读(4)  评论(0)    收藏  举报