Go语言面试遇到,面试官问什么是协程、什么是协程泄漏和数组跟切片是用该如何回答

📅 2026/7/1 2:08:22
Go语言面试遇到,面试官问什么是协程、什么是协程泄漏和数组跟切片是用该如何回答
什么是协程协程我把它理解为用户态的轻量级线程。它和线程最大的区别在于线程的调度由操作系统内核负责切换时涉及内核态和用户态的切换开销比较大而协程的调度完全在用户态完成由程序自己控制什么时候挂起、什么时候恢复因此切换成本极低。从本质上讲协程是一种可暂停、可恢复的函数。普通函数一旦执行就一路到底中间没法停下来让给别人但协程可以在执行到一半时主动让出执行权比如遇到 I/O 等待等条件满足了再回到刚才的断点继续往下跑。什么是协程泄漏协程泄漏简单说就是协程没有正常结束但它的引用已经丢了或者说它一直被阻塞在某个地方永远等不到结果导致占用的内存和其他资源无法释放。和内存泄漏类比内存泄漏是’分配了内存忘了 free’协程泄漏就是’创建了协程忘了或者说没法让它正常结束’。举个例子我启动了一个协程去等待一个网络请求的超时结果但这个请求的 future 永远没有被 resolve 也没有被 cancel那么这个协程就会一直挂在那里——它占着栈空间和闭包引用的变量GC 也无法回收随着时间推移越积越多最终撑爆内存或端口耗尽。场景一永不关闭的 channel// ❌ 泄漏ch 永远没人发也不关闭这个 goroutine 永远阻塞 func leak() { ch : make(chan int) go func() { val : -ch // 永远在这儿等着 fmt.Println(val) }() // 函数返回了但上面的 goroutine 还活着 }场景二往没人读的 channel 发数据// ❌ 泄漏goroutine 想往 channel 发数据但没人接 func leak() { ch : make(chan int) // 无缓冲 go func() { ch - 42 // 永远阻塞因为没人来读 }() }场景三for-range 未关闭的 channel// ❌ 泄漏channel 不 closerange 就永远不会结束 func leak() { ch : make(chan int) go func() { for val : range ch { // ch 一直开着range 出不去 fmt.Println(val) } }() }场景四select 缺少退出通道// ❌ 泄漏select 没有 case 能退出 func leak(ctx context.Context) { ch : make(chan int) go func() { for { select { case v : -ch: handle(v) // 少了 case -ctx.Done(): return } } }() }场景五http.Client 不读 Body// ❌ 经典泄漏http.Get 之后没读也没关 Body连接无法复用 func leak() { for { resp, _ : http.Get(https://example.com) // 忘了 resp.Body.Close() → goroutine 泄漏 连接泄漏 } }什么是数组Go 的数组是一块固定长度、类型确定的连续内存区域。长度是类型的一部分——[3]int和[5]int在 Go 眼里是完全不同的类型不能互相赋值。核心特点就三点长度固定编译期就确定了之后不能变值类型赋值或传参会发生整体拷贝不是传引用内存连续所有元素紧挨着放在一起CPU 缓存友好举个例子var arr [3]int [3]int{1, 2, 3} arr2 : arr // 整个数组拷贝了一份arr 和 arr2 互不影响 arr2[0] 99 // arr 还是 [1, 2, 3]实际开发中我几乎不会直接用[N]T这种数组。它更多出现在底层——比如[32]byte出现在 SHA256 的返回类型里或者作为切片的底层存储。什么是切片切片是对数组的一个动态视图。它本身不存数据存的是三样东西指向底层数组某个位置的指针、长度len、容量cap。type slice struct { ptr *Element // 指向底层数组的指针 len int // 当前有多少个元素 cap int // 从 ptr 开始到数组末尾还能装多少个 }因为这个设计切片有几个重要行为它是引用语义——赋值或传参只拷贝 slice header24 字节不拷贝底层数据append 扩容当 len cap 时append 会分配一块更大的新底层数组把旧数据拷过去返回新切片通过make([]T, len, cap)可以预分配容量避免频繁扩容切片是我在 Go 里最常用的数据结构基本取代了数组。函数传参、返回集合、处理子串全是切片。区别是什么对比维度数组[N]T切片[]T长度固定是类型的一部分动态len 可以变化类型[3]int≠[5]int[]int就是[]int不管 len 多少值/引用值类型赋值全量拷贝引用语义赋值只拷贝 header内存位置自身就是数据自身只是 header数据在底层数组传参开销整个数组复制一份大数组很贵只拷贝 24 字节 header可以用比较吗可以同类型不能直接用只能和 nil 比nil数组没有 nil切片零值是 nil例子一值拷贝 vs 引用语义// 数组值拷贝互不影响 arr1 : [3]int{1, 2, 3} arr2 : arr1 arr2[0] 999 fmt.Println(arr1[0]) // 1 ← 没变 // 切片共享底层数组 s1 : []int{1, 2, 3} s2 : s1 s2[0] 999 fmt.Println(s1[0]) // 999 ← 变了例子二函数传参// ❌ 数组传参100万 int 全拷贝4MB 复制 func sumArray(arr [1000000]int) int { ... } // ✅ 切片传参只传 24 字节 func sumSlice(s []int) int { ... }例子三append 的陷阱s1 : make([]int, 0, 3) s2 : s1 s2 append(s2, 1, 2, 3) // 还在 cap 之内s1 底层数组也变了 s2 append(s2, 4) // 超 cap扩容了s2 换新底层数组s1 不受影响