今日面试遇到golang相关的问题,所以让ai总结整理了 Go 语言常见面试题,涵盖基础、并发、内存管理、标准库等核心知识点。
一、基础语法与特性
1. Go 与 Python/Java 的核心区别
| 特性 |
Go |
Python |
Java |
| 类型系统 |
静态类型,编译型 |
动态类型,解释型 |
静态类型,编译型 |
| 并发模型 |
Goroutine + Channel |
线程/协程(asyncio) |
线程/线程池 |
| 内存管理 |
GC,但可手动优化 |
GC |
GC |
| 编译速度 |
极快 |
无需编译 |
较慢 |
| 部署 |
单二进制文件 |
依赖解释器 |
JRE + jar |
| 错误处理 |
显式 error 返回值 |
异常 try/except |
异常 try/catch |
2. 值传递 vs 引用传递
go
复制代码
// Go 只有值传递!但指针、slice、map、channel 是引用类型
func modifyValue(x int) {
x = 100 // 修改的是副本,不影响原值
}
func modifyPointer(x *int) {
*x = 100 // 修改指针指向的值,影响原值
}
func modifySlice(s []int) {
s[0] = 100 // slice 是引用类型,会影响原值!
s = append(s, 200) // 但 append 可能触发扩容,此时 s 指向新数组,不影响原值
}
// 面试陷阱题
func main() {
s := []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // 输出 [100 2 3],不是 [1 2 3]!
}
3. defer 的执行顺序和陷阱
go
复制代码
// defer 是 LIFO(后进先出)
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// 输出: 3 2 1
}
// 陷阱:defer 参数在定义时求值
func trap() {
i := 0
defer fmt.Println(i) // 输出 0,不是 3!
i = 3
}
// 陷阱:defer 与 return 的执行顺序
func deferAndReturn() (result int) {
defer func() {
result++ // 可以修改命名返回值!
}()
return 0 // 实际返回 1
}
// 性能陷阱:循环内 defer
func bad() {
for i := 0; i < 10000; i++ {
defer fmt.Println(i) // 内存泄漏!defer 不会立即执行
}
}
二、并发编程(重点)
4. Goroutine 和 Channel
go
复制代码
// Goroutine 是用户态轻量级线程,~2KB栈,可动态增长
// 由 Go runtime 调度,非 OS 线程
// Channel 是 Goroutine 间通信的方式(CSP 模型)
// 基础用法
func basicChannel() {
ch := make(chan int, 3) // 缓冲通道,容量3
go func() {
ch <- 1 // 发送
ch <- 2
ch <- 3
close(ch) // 关闭通道
}()
for v := range ch { // 遍历直到关闭
fmt.Println(v)
}
}
// 面试常考:无缓冲 vs 有缓冲
func bufferedVsUnbuffered() {
// 无缓冲:同步通信,发送和接收必须同时准备好
unbuf := make(chan int)
go func() { unbuf <- 1 }() // 发送会阻塞,直到有人接收
<-unbuf // 接收会阻塞,直到有人发送
// 有缓冲:异步通信,满时发送阻塞,空时接收阻塞
buf := make(chan int, 1)
buf <- 1 // 不阻塞,立即返回
buf <- 2 // 阻塞!缓冲区满
}
5. select 多路复用
go
复制代码
func selectDemo() {
ch1 := make(chan int)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "hello"
}()
// 同时监听多个通道
select {
case v1 := <-ch1:
fmt.Println("ch1:", v1)
case v2 := <-ch2:
fmt.Println("ch2:", v2)
case <-time.After(3 * time.Second): // 超时
fmt.Println("timeout")
default: // 非阻塞,立即执行
fmt.Println("no data")
}
// 面试常考:select 的随机性
// 当多个 case 同时就绪时,select 随机选择一个!
}
6. 并发同步原语
go
复制代码
import "sync"
// WaitGroup:等待一组 Goroutine 完成
func waitGroupDemo() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1) // 增加计数
go func(id int) {
defer wg.Done() // 完成时减少计数
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // 阻塞直到计数为0
}
// Mutex:互斥锁
func mutexDemo() {
var mu sync.Mutex
var count int
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
count++ // 临界区
mu.Unlock()
}()
}
}
// RWMutex:读写锁,读多写少场景
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // 读锁,多个读并发
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // 写锁,独占
defer c.mu.Unlock()
c.data[key] = value
}
// Once:只执行一次(单例模式)
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
// Pool:对象池,减少 GC 压力
var bytePool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
func usePool() {
buf := bytePool.Get().([]byte) // 获取
// 使用...
bytePool.Put(buf) // 归还,复用
}
7. Context 上下文控制
go
复制代码
// Context 是 Go 并发的灵魂,用于传递取消信号、超时、元数据
func contextDemo() {
// 1. 超时控制
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second):
fmt.Println("work done")
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err()) // context deadline exceeded
}
// 2. 手动取消
ctx2, cancel2 := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Second)
cancel2() // 通知所有监听 ctx2.Done() 的 Goroutine 退出
}()
// 3. 传递元数据
ctx3 := context.WithValue(context.Background(), "userID", "123")
userID := ctx3.Value("userID").(string)
}
// 面试常考:Context 传递最佳实践
func handleRequest(ctx context.Context, req *Request) {
// 每个需要异步处理的函数都接收 ctx 作为第一个参数
// 形成树状结构,根 Context 取消时,所有子 Context 都收到信号
go queryDatabase(ctx, req.Query)
go callExternalAPI(ctx, req.API)
// 两者都会响应 ctx 的取消信号
}
8. 常见并发陷阱
go
复制代码
// 陷阱1:闭包捕获循环变量
func closureTrap() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i) // 错误!所有 Goroutine 打印相同的值(大概率是10)
}()
}
// 正确:传参
for i := 0; i < 10; i++ {
go func(id int) {
fmt.Println(id) // 正确
}(i)
}
}
// 陷阱2:向已关闭的 Channel 发送
func closedChannelTrap() {
ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel
}
// 陷阱3:Channel 泄露
func leakTrap() {
ch := make(chan int)
go func() {
// 如果这里 panic 或提前返回,无人接收,发送方永远阻塞
ch <- 1
}()
// 忘记 <-ch
}
// 陷阱4:死锁
func deadlockTrap() {
ch := make(chan int) // 无缓冲
ch <- 1 // 死锁!无接收方,永远阻塞
<-ch
}
三、内存管理与 GC
9. 逃逸分析
go
复制代码
// 逃逸分析:编译器决定变量分配在栈还是堆
func stackAlloc() int {
x := 10 // 栈分配,函数返回后自动释放
return x
}
func heapAlloc() *int {
x := 10
return &x // 逃逸到堆!函数返回后仍需访问
}
// 查看逃逸分析:go build -gcflags="-m"
// 常见逃逸场景:
// 1. 返回指针
// 2. 闭包引用外部变量
// 3. 向 interface{} 传值
// 4. 切片/Map 扩容超过栈容量
10. GC 机制
go
复制代码
// Go 使用三色标记 + 混合写屏障的并发 GC
// GC 调优参数
// GOGC=100 默认,堆增长100%触发GC
// GOGC=off 关闭GC(仅用于调试)
// 减少 GC 压力的技巧
func reduceGCPressure() {
// 1. 对象池(见 sync.Pool)
// 2. 预分配切片容量,避免频繁扩容
data := make([]int, 0, 10000) // 预分配
// 3. 大对象使用指针,避免值拷贝
type BigStruct struct {
data [1024 * 1024]int // 1MB
}
func process(ptr *BigStruct) { // 传指针,8字节
// 而不是 process(s BigStruct),拷贝1MB
}
}
四、接口与反射
11. 接口实现机制
go
复制代码
// Go 接口是隐式实现,鸭子类型
type Reader interface {
Read(p []byte) (n int, err error)
}
// 不需要声明 implements,只要有 Read 方法就自动实现
type MyReader struct{}
func (m MyReader) Read(p []byte) (n int, err error) {
return 0, nil
}
// 面试常考:空接口 interface{}
func emptyInterface() {
var i interface{} = "hello"
// 类型断言
s := i.(string) // 失败时 panic
s, ok := i.(string) // 安全断言,ok=false 时不 panic
// 类型 switch
switch v := i.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
default:
fmt.Println("unknown:", v)
}
}
// 面试常考:nil 接口
type MyError struct{}
func (e *MyError) Error() string { return "error" }
func returnNilError() error {
var e *MyError = nil
return e // 返回的是 (type=*MyError, value=nil) 的接口,不是 nil!
}
func main() {
err := returnNilError()
fmt.Println(err == nil) // false!陷阱!
}
12. 反射
go
复制代码
import "reflect"
func reflectDemo() {
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
u := User{Name: "Tom", Age: 20}
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
// 遍历字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("%s: %v (tag: %s)\n", field.Name, value, field.Tag.Get("json"))
}
// 修改值(必须传指针)
v2 := reflect.ValueOf(&u).Elem()
v2.FieldByName("Age").SetInt(30)
}
// 反射的性能代价:比直接调用慢 10-100 倍
// 生产环境尽量避免在热路径使用反射
五、标准库与工程实践
13. HTTP 服务
go
复制代码
package main
import (
"net/http"
"time"
)
// 标准库实现
func basicHTTPServer() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil) // 默认无超时,生产勿用!
}
// 生产级配置
func productionHTTPServer() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
server.ListenAndServe()
}
// 中间件模式
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL, time.Since(start))
})
}
14. 测试
go
复制代码
// 单元测试
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d, want 5", result)
}
}
// 表驱动测试(Go 惯用法)
func TestAddTable(t *testing.T) {
tests := []struct {
a, b, want int
}{
{2, 3, 5},
{-1, 1, 0},
{0, 0, 0},
}
for _, tt := range tests {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
}
}
// Benchmark
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// go test -bench=.
// Mock 接口
type Database interface {
Get(id string) (User, error)
}
type MockDB struct {
data map[string]User
}
func (m *MockDB) Get(id string) (User, error) {
return m.data[id], nil
}
六、高频面试题速查
简答题
| 问题 |
核心要点 |
| Goroutine 为什么轻量? |
用户态调度,~2KB栈,动态增长,非OS线程 |
| Channel 底层实现? |
环形队列 + 锁 + 发送/接收队列 |
| Go 的 map 是线程安全的吗? |
不是!需用 sync.RWMutex 或 sync.Map |
| 什么情况下会发生内存泄漏? |
Goroutine 泄露、未关闭的 Channel、全局变量累积 |
| defer 的用途和陷阱? |
资源清理、参数求值时机、循环内使用 |
| 如何实现单例模式? |
sync.Once、init、原子操作 |
| 两个 Goroutine 如何交替打印? |
两个无缓冲 Channel 互相通知 |
代码题
go
复制代码
// 题1:交替打印数字和字母
// 输出: 1A2B3C4D...
func alternatePrint() {
numCh := make(chan bool)
charCh := make(chan bool)
done := make(chan bool)
go func() {
for i := 1; i <= 4; i++ {
<-numCh // 等待信号
fmt.Print(i)
charCh <- true // 通知打印字母
}
}()
go func() {
for c := 'A'; c <= 'D'; c++ {
<-charCh
fmt.Printf("%c", c)
numCh <- true
}
done <- true
}()
numCh <- true // 启动
<-done
}
// 题2:实现一个线程安全的 Map
type SafeMap struct {
mu sync.RWMutex
data map[string]interface{}
}
func (s *SafeMap) Get(key string) (interface{}, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.data[key]
return val, ok
}
func (s *SafeMap) Set(key string, val interface{}) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = val
}
// 题3:控制并发数量(信号量模式)
func limitedConcurrency(urls []string, maxConcurrent int) {
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
sem <- struct{}{} // 获取信号量
go func(u string) {
defer wg.Done()
defer func() { <-sem }() // 释放信号量
fetch(u)
}(url)
}
wg.Wait()
}
学习资源推荐