Go 关键字:select / defer / panic & recover / make & new

一、select:Channel 世界的"多路复用器"

1. select 是什么?

select 和 switch 语法长得有点像,但它只 能用在 channel 的收发操作上,可以在多个 channel 间做"同时监听":

go 复制代码
select {
case v := <-ch1:
    // 从 ch1 收到数据
case ch2 <- x:
    // 向 ch2 发送数据
default:
    // 所有 case 都不准备好时的兜底分支(非阻塞)
}

2. 必须记住的几个行为

  • 空 select{} 会永久阻塞

    go 复制代码
    select {} // 当前 goroutine 永远阻塞,CPU 不再执行后续代码
  • 只有一个 case 时,编译器会把它"改写成 if + 阻塞收发"

    go 复制代码
    // 源码
    select {
    case v := <-ch:
        use(v)
    }
    
    // 等价改写思路(伪代码)
    if ch == nil {
        block() // 永远阻塞
    }
    v := <-ch     // 阻塞等待
    use(v)
  • 配合 default 可以实现非阻塞收发

    go 复制代码
    select {
    case v := <-ch:
        fmt.Println("got", v)
    default:
        fmt.Println("no data, do something else")
    }
  • 多个 case 同时就绪时,Go 会随机选一个执行
    没有优先级,不要依赖执行顺序 来写逻辑。

三、panic & recover:Go 的"异常"机制

1. panic:程序进入"异常"状态

  • 可以主动调用 :panic("something wrong")
  • 也可以由运行时触发:例如数组越界、空指针等

一旦某个函数里发生 panic:

  1. 当前函数立刻停止后续普通代码的执行;
  2. 依次执行当前函数中已经注册的 defer;
  3. 返回到上一层调用者,继续做同样的"执行 defer → 返回";
  4. 如果一路都没人 recover 它,最终由 runtime 打印堆栈并终止程序。

注意:panic 自己不打印任何东西 ,真正打印信息的是最外层 runtime。

另外,对于标准 Go 编译器,有些错误是无法被 recover 捕获的(比如栈溢出、OOM 等),程序会直接崩溃。

2. recover:从 panic 中"拉回现场"

recover 只能在 defer 的函数里调用才有效:

go 复制代码
func safeRun(fn func()) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("caught panic:", r)
        }
    }()
    fn()
}

几点要点:

  • recover() 返回 nil 的几种情况:
    • 没有发生 panic;
    • 发生 panic,但你不是在 defer 里调用;
    • 发生 panic,但 panic 的值本身就是 nil。
  • panic 跨 goroutine 无效 :在一个 goroutine 里 panic,另一个 goroutine 里的 recover 捕不到。

四、make & new:两种完全不同的"分配方式"

1. new:给任意类型分配一块零值内存

new(T) 做的事情:

  • 为类型 T 分配一块内存;
  • 将这块内存置为 零值;
  • 返回 *T(指向这块零值内存的指针)。

示例:

go 复制代码
type User struct {
    Name string
    Age  int
}

u := new(User) // *User,字段都是零值
fmt.Println(u.Name, u.Age) // "" 0

几乎任何类型 都可以用 new,但对复合类型(map、slice、chan)来说,只是"得到一个指针指向零值",并不保证它已经可用。

2. make:为内建引用类型做"完整初始化"

make 只用于三种类型:

  • slice
  • map
  • chan

它会:

  • 分配底层数据结构的内存;
  • 做好必要的初始化;
  • 返回 非零值 的该类型,而不是指针。

2.1 为什么 slice / map / chan 要用 make?

  • 对 map:

    go 复制代码
    var m map[string]int     // == nil
    // m["a"] = 1            // 这里直接赋值会 panic
    
    m = make(map[string]int) // 完整初始化
    m["a"] = 1               // OK
  • 对 chan:

    go 复制代码
    var ch chan int // == nil
    // <-ch         // 这里会永久阻塞
    
    ch = make(chan int, 1)
    ch <- 1        // OK
  • 对 slice:

    go 复制代码
    var s []int      // == nil,但可以被 append 使用
    s = append(s, 1) // Go 会自动为它分配底层数组

    尽管 nil slice 也能直接 append,但在实际工程中,我们经常需要:

    • 提前确定容量,减少扩容开销;
    • 明确区分"未初始化"和"已初始化但空"两种语义。

    这时最好用:

    go 复制代码
    s := make([]int, 0, 16)

3. 总结:什么时候用谁?

  • 想要一个任意类型的"零值指针" :用 new(T);
  • 想要一个可以直接用来读写的 slice/map/chan 值 :用 make;
  • 其他情况大部分只需要用字面量或普通变量声明,不必刻意用 new。
相关推荐
明月_清风4 分钟前
AI Agent 进入系统层:现在有哪些开源项目在解决这个问题?
人工智能·后端
弈栈录31 分钟前
MySQL 事务、索引与锁:后端开发必须掌握的数据库基础
数据库·后端
明月_清风32 分钟前
AI Agent 最大的问题,可能不是智商,而是“权限”
人工智能·后端
vx_Biye_Design35 分钟前
expressDeepSeek社团咨询助手的学生社团管理系统60746-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·课程设计·express
长安米粒贵36 分钟前
缓存明明加了,为什么数据还是不一致?聊聊 4 个容易忽略的边界
后端
yunwei3740 分钟前
eBPF 开发实践:使用 eBPF 隐藏进程或文件信息
linux·后端·性能优化
高频因子挖掘机41 分钟前
行情监控脚本重启后,怎样快速恢复而不重复拉取数据?
后端·github·api
aleafboat43 分钟前
只管去写(day2) 用claude分析老项目技术栈
后端
vx_Biye_Design1 小时前
springboot宠物领养与救助平台64334-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·mysql·课程设计·宠物
用户7713970207061 小时前
.NET 依赖注入入门
后端