面试题答案
一键面试1. Gomake 和 new 的基础区别
new(T)
会为类型 T
分配零值内存并返回指针。而 make(T, args)
用于创建 slice、map 和 channel,会初始化数据结构并返回非指针类型(对于 slice 和 map 而言,它们内部包含指针,但返回值不是指针)。
2. 并发访问时可能遇到的问题及底层原理
数据竞争
- new 创建的数据结构:如果
new
创建的是自定义结构体且结构体中包含共享可变数据,多个 goroutine 同时读写这些数据,就可能产生数据竞争。从内存模型角度,Go 的内存模型规定了在没有同步机制的情况下,并发读写共享变量会导致未定义行为。调度器在切换 goroutine 时,可能会在读写操作未完成时就切换上下文,导致数据不一致。 - make 创建的数据结构:
- map:Go 语言的 map 不是线程安全的。多个 goroutine 同时读写 map 时,会导致数据竞争。底层实现上,map 的实现是基于哈希表,在并发读写时可能导致哈希表结构损坏。
- slice:如果只是读操作,一般不会有问题。但如果多个 goroutine 同时对 slice 进行写操作,比如 append 操作,会导致数据竞争,因为 append 可能会重新分配内存,并发操作会导致内存访问冲突。
- channel:channel 本身是线程安全的,多个 goroutine 对其进行发送和接收操作不会产生数据竞争,因为其内部实现通过锁和队列来保证操作的原子性。
资源泄漏
- new 创建的数据结构:如果
new
创建的结构体中包含需要手动释放的资源(如文件句柄等),且在并发环境中没有正确释放,就可能导致资源泄漏。从调度器角度,某个 goroutine 持有资源但异常退出,而调度器无法感知并清理这些资源。 - make 创建的数据结构:
- map:理论上不会直接导致资源泄漏,因为 map 会在其生命周期结束时被垃圾回收器回收。但如果 map 中持有外部资源(如文件句柄等)且未正确清理,也可能导致资源泄漏。
- slice:类似 map,如果 slice 中持有需要手动释放的资源且未正确释放,可能导致资源泄漏。
- channel:如果 channel 没有被正确关闭,可能会导致 goroutine 永远阻塞在发送或接收操作上,从而造成资源泄漏。
3. 并发场景示例代码及分析
new 创建结构体并发访问示例
package main
import (
"fmt"
)
type Counter struct {
Value int
}
func main() {
counter := new(Counter)
var numGoroutines = 1000
for i := 0; i < numGoroutines; i++ {
go func() {
counter.Value++
}()
}
fmt.Println(counter.Value)
}
问题及原因:上述代码中多个 goroutine 并发对 counter.Value
进行写操作,会产生数据竞争。因为没有同步机制,调度器可能在一个 goroutine 尚未完成对 counter.Value
的写操作时就切换到另一个 goroutine 进行写操作,导致最终结果不确定。
make 创建 map 并发访问示例
package main
import (
"fmt"
)
func main() {
myMap := make(map[string]int)
var numGoroutines = 1000
for i := 0; i < numGoroutines; i++ {
go func(key string) {
myMap[key] = i
}(fmt.Sprintf("key%d", i))
}
fmt.Println(myMap)
}
问题及原因:多个 goroutine 并发对 myMap
进行写操作,会导致数据竞争。map 的底层哈希表结构在并发写时可能会被破坏,导致程序出现未定义行为,例如崩溃或数据丢失。
make 创建 channel 资源泄漏示例
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
for {
data := <-ch
fmt.Println(data)
}
}()
time.Sleep(2 * time.Second)
}
问题及原因:上述代码中,ch
没有被关闭,导致在 main
函数结束时,<-ch
处的 goroutine 会永远阻塞,造成资源泄漏。因为没有关闭信号,这个 goroutine 会一直占用系统资源。