Go 面试八股速通(C++ 程序员版)
面向 C++ 工程师整理 8 个 Go 面试高频问题,配套可运行示例代码与回答思路
Go 面试八股速通(C++ 程序员版)
如果你是 C++ 程序员,准备转 Go 岗位面试,最容易卡住的不是语法,而是语言设计取舍。这篇文章用“八股问答 + 小代码”的方式,帮你快速建立 Go 面试表达框架。
建议回答结构:
- 先说一句结论;
- 再和 C++ 做对比;
- 最后给一个工程实践建议。
1. Go 的值传递和引用语义怎么理解?
一句话版:Go 只有值传递,但有些值里“包着指针”(如 slice、map、chan、interface),看起来像引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "fmt"
func changeArray(a [3]int) {
a[0] = 100
}
func changeSlice(s []int) {
s[0] = 100
}
func main() {
a := [3]int{1, 2, 3}
s := []int{1, 2, 3}
changeArray(a)
changeSlice(s)
fmt.Println("array:", a) // [1 2 3]
fmt.Println("slice:", s) // [100 2 3]
}
面试可展开:
- 数组
[N]T是完整值,拷贝成本高,行为更像 C++ 的std::array按值传参。 slice是三元描述符(指针、长度、容量),按值拷贝的是描述符,但底层数组共享。- 工程上,大对象优先用指针或 slice,避免无意义拷贝。
2. slice 和数组的区别?扩容发生了什么?
一句话版:数组定长且值语义;slice 是动态视图,append 可能触发扩容并更换底层数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
s := make([]int, 0, 2)
s = append(s, 1, 2)
alias := s
s = append(s, 3) // 触发扩容后,s 可能指向新数组
alias[0] = 99
fmt.Println("alias:", alias) // [99 2]
fmt.Println("s:", s) // [1 2 3] 或 [99 2 3](取决于是否扩容)
}
面试可展开:
- 重点不是背扩容倍数,而是知道 append 后旧切片可能“脱钩”。
- 类比 C++
vector:扩容后迭代器/引用可能失效。 - 传 slice 给函数并不总是“安全修改原数据”,要看是否重新分配。
3. map 为什么并发不安全?怎么解决?
一句话版:Go 原生 map 不是并发容器;并发读写会 panic 或数据竞争。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.RWMutex
m map[string]int
}
func (c *Counter) Inc(k string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[k]++
}
func (c *Counter) Get(k string) int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.m[k]
}
func main() {
c := &Counter{m: map[string]int{}}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc("go")
}()
}
wg.Wait()
fmt.Println(c.Get("go"))
}
面试可展开:
- 读多写少可用
RWMutex,高竞争可评估分片锁或sync.Map。 - C++ 常见方案是
unordered_map + mutex,思路一致。 - 面试加分点:顺带提
go test -race检测数据竞争。
4. goroutine 和 OS 线程是什么关系?
一句话版:goroutine 是用户态轻量协程,由 Go 调度器映射到少量内核线程(G-M-P 模型)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"fmt"
"time"
)
func worker(id int) {
for i := 0; i < 3; i++ {
fmt.Printf("worker-%d: %d\n", id, i)
time.Sleep(10 * time.Millisecond)
}
}
func main() {
for i := 0; i < 3; i++ {
go worker(i)
}
time.Sleep(100 * time.Millisecond)
}
面试可展开:
- C++
std::thread通常一对一映射内核线程,创建和切换成本更高。 - goroutine 初始栈小且可增长,适合高并发 I/O。
- 但 goroutine 不是“免费”:阻塞、泄漏、无界创建都会出问题。
5. channel 的本质和常见坑?
一句话版:channel 是带同步语义的队列;无缓冲强调“交接”,有缓冲强调“削峰”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v)
}
// 再发送会 panic:send on closed channel
// ch <- 3
}
面试可展开:
- 关闭原则:通常由发送方关闭,且只关闭一次。
- 从已关闭 channel 接收会立刻返回零值,可配合
v, ok := <-ch判断。 - 用 channel 不代表不需要锁,状态共享场景仍可能需要 mutex。
6. defer 的执行时机与代价?
一句话版:defer 在函数返回前按 LIFO 执行,参数在 defer 声明时求值。
1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main() {
x := 1
defer fmt.Println("defer x:", x) // 这里已拷贝 x=1
x = 2
fmt.Println("now x:", x)
}
输出:
1
2
now x: 2
defer x: 1
面试可展开:
- 最常用于文件关闭、解锁、埋点收尾。
- 在极高频热路径里,大量 defer 可能有额外开销,需要 profiling 再优化。
- 类比 C++ RAII:一个是语法机制,一个是对象生命周期机制,目标都是“别忘记释放资源”。
7. new 和 make 的区别?
一句话版:new(T) 返回 *T 零值指针;make 用于初始化 slice/map/chan 并返回其本体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
p := new(int)
fmt.Println(*p) // 0
m := make(map[string]int)
m["k"] = 1
s := make([]int, 3, 5)
ch := make(chan int, 1)
ch <- 42
fmt.Println(m, s, <-ch)
}
面试可展开:
- 不能对 map/slice/chan 只用
new就直接使用其语义能力。 var m map[string]int仅声明 nil map,写入会 panic。- 这是 Go 面试非常爱考的“可用性初始化”问题。
8. 垃圾回收(GC)会不会让延迟不可控?
一句话版:Go 是并发三色标记清除 GC,延迟通常可控,但你仍要减少逃逸和短命对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
type Big struct {
buf [1024]byte
}
func alloc(n int) []*Big {
res := make([]*Big, 0, n)
for i := 0; i < n; i++ {
res = append(res, &Big{})
}
return res
}
func main() {
x := alloc(10000)
fmt.Println(len(x))
}
面试可展开:
- C++ 习惯手工控制生命周期,Go 则更依赖逃逸分析和 GC。
- 可用
go build -gcflags="-m"观察逃逸,用 pprof 看分配热点。 - 优化优先级:先减少分配,再谈对象池(如
sync.Pool),避免过早复杂化。
面试收尾话术(给 C++ 程序员)
你可以这样总结:
Go 不是在“性能绝对值”上替代 C++,而是在工程效率、并发模型、交付稳定性上给出更高性价比。我的迁移策略是:复用 C++ 的工程化习惯(抽象、测试、性能分析),接受 Go 在内存与并发语义上的新约束。
这句总结能体现两点:你理解 Go 的设计哲学,也保留了 C++ 工程师的优势。
本文由作者按照 CC BY 4.0 进行授权