面试题答案
一键面试资源竞争问题
- 共享变量:闭包可能会引用外部作用域的变量,若多个 goroutine 同时访问和修改这些共享变量,会引发资源竞争。例如:
package main
import (
"fmt"
)
func main() {
num := 0
var funcs []func()
for i := 0; i < 10; i++ {
funcs = append(funcs, func() {
num = num + 1
fmt.Println(num)
})
}
for _, f := range funcs {
go f()
}
// 这里为了让程序运行一会以便看到并发效果,实际应用中可以采用更优雅的同步方式
select {}
}
在此例中,num
是共享变量,多个 goroutine 同时修改它,结果不可预测。解决办法是使用互斥锁(sync.Mutex
):
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
num := 0
var funcs []func()
for i := 0; i < 10; i++ {
funcs = append(funcs, func() {
mu.Lock()
num = num + 1
fmt.Println(num)
mu.Unlock()
})
}
for _, f := range funcs {
go f()
}
select {}
}
- 数据结构:对于复杂的数据结构,如 map,在并发闭包中使用也需注意。Go 语言的 map 不是线程安全的,若多个 goroutine 通过闭包同时读写 map,会导致程序崩溃。例如:
package main
import (
"fmt"
)
func main() {
m := make(map[string]int)
var funcs []func()
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key%d", i)
funcs = append(funcs, func() {
m[key] = i
fmt.Println(m[key])
})
}
for _, f := range funcs {
go f()
}
select {}
}
解决方法是使用 sync.Map
,它是线程安全的 map 实现:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var funcs []func()
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key%d", i)
funcs = append(funcs, func() {
m.Store(key, i)
value, _ := m.Load(key)
fmt.Println(value)
})
}
for _, f := range funcs {
go f()
}
select {}
}
内存管理问题
- 闭包捕获变量的生命周期:闭包会延长它所捕获变量的生命周期。若闭包持有大对象的引用,且该闭包在 goroutine 中长期运行,可能导致内存无法及时释放。例如:
package main
import (
"fmt"
"time"
)
func bigObjectGenerator() []byte {
return make([]byte, 1024*1024) // 生成 1MB 的大对象
}
func main() {
var funcs []func()
for i := 0; i < 10; i++ {
data := bigObjectGenerator()
funcs = append(funcs, func() {
fmt.Println(len(data))
time.Sleep(time.Second)
})
}
for _, f := range funcs {
go f()
}
time.Sleep(2 * time.Second)
}
在此例中,data
是大对象,闭包持有其引用,在 goroutine 运行期间,这些内存不会被释放。优化方法是在闭包使用完数据后,将其设置为 nil
,以便垃圾回收器回收内存:
package main
import (
"fmt"
"time"
)
func bigObjectGenerator() []byte {
return make([]byte, 1024*1024)
}
func main() {
var funcs []func()
for i := 0; i < 10; i++ {
data := bigObjectGenerator()
funcs = append(funcs, func() {
fmt.Println(len(data))
data = nil
time.Sleep(time.Second)
})
}
for _, f := range funcs {
go f()
}
time.Sleep(2 * time.Second)
}
- 避免无意义的闭包嵌套:多层闭包嵌套可能会使代码逻辑复杂,同时增加内存开销。因为每一层闭包都可能捕获外部变量,导致更多的内存占用。例如:
package main
import "fmt"
func outer() func() {
num := 10
return func() {
inner := func() {
fmt.Println(num)
}
inner()
}
}
func main() {
f := outer()
f()
}
在此例中,inner
闭包嵌套在 outer
返回的闭包内,这种嵌套可能是不必要的,可简化为:
package main
import "fmt"
func outer() func() {
num := 10
return func() {
fmt.Println(num)
}
}
func main() {
f := outer()
f()
}
闭包与 goroutine 泄漏
- 未结束的 goroutine:若闭包启动的 goroutine 没有正确结束,会导致 goroutine 泄漏。例如:
package main
import (
"fmt"
"time"
)
func main() {
var funcs []func()
for i := 0; i < 10; i++ {
funcs = append(funcs, func() {
go func() {
for {
fmt.Println("Leaking goroutine")
time.Sleep(time.Second)
}
}()
})
}
for _, f := range funcs {
f()
}
time.Sleep(2 * time.Second)
}
在这个例子中,闭包内启动的 goroutine 是一个无限循环,不会结束,导致 goroutine 泄漏。解决方法是提供一种退出机制,如使用 context.Context
:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var funcs []func()
for i := 0; i < 10; i++ {
funcs = append(funcs, func() {
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
fmt.Println("Running goroutine")
time.Sleep(time.Second)
}
}
}(ctx)
})
}
for _, f := range funcs {
f()
}
time.Sleep(3 * time.Second)
}