MST

星途 面试题库

面试题:Go 中 Gomake 与 new 在并发编程场景下的深度剖析

在并发编程环境中,使用 Gomake 和 new 创建的数据结构在多个 goroutine 并发访问时,可能会遇到哪些不同的问题?比如数据竞争、资源泄漏等。如何从底层原理(如内存模型、调度器等方面)来解释这些差异?请给出具体的并发场景示例代码,并分析可能出现的问题及原因。
19.3万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

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 会一直占用系统资源。