MST

星途 面试题库

面试题:Go语言数组与切片在并发场景下的使用陷阱与应对方案

在并发编程环境中,Go语言的数组和切片可能会遇到哪些使用陷阱?请举例说明,并提出具体的应对方案和最佳实践。
44.2万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

1. 并发读写未同步

  • 陷阱举例:多个 goroutine 同时读写同一个切片,会导致数据竞争。例如:
package main

import (
    "fmt"
)

var s = make([]int, 0)

func write() {
    for i := 0; i < 10; i++ {
        s = append(s, i)
    }
}

func read() {
    for _, v := range s {
        fmt.Println(v)
    }
}

func main() {
    go write()
    go read()
    select {}
}

此代码在多个 goroutine 同时读写 s 切片时,会引发数据竞争问题,运行结果可能是不可预测的。

  • 应对方案:使用 sync.Mutex 进行同步。修改后的代码如下:
package main

import (
    "fmt"
    "sync"
)

var (
    s   = make([]int, 0)
    mux sync.Mutex
)

func write() {
    for i := 0; i < 10; i++ {
        mux.Lock()
        s = append(s, i)
        mux.Unlock()
    }
}

func read() {
    mux.Lock()
    for _, v := range s {
        fmt.Println(v)
    }
    mux.Unlock()
}

func main() {
    go write()
    go read()
    select {}
}
  • 最佳实践:尽量减少共享数据,通过消息传递(如使用 channels)来在 goroutine 间通信,避免显式的锁操作,使代码更简洁且易于维护。

2. 切片扩容导致数据覆盖

  • 陷阱举例:当多个 goroutine 同时对一个切片进行追加操作时,如果切片需要扩容,可能会导致数据覆盖。例如:
package main

import (
    "fmt"
)

var s = make([]int, 0, 5)

func appendData() {
    for i := 0; i < 10; i++ {
        s = append(s, i)
    }
}

func main() {
    for i := 0; i < 2; i++ {
        go appendData()
    }
    select {}
}

由于两个 goroutine 同时进行追加操作,可能会导致切片扩容时数据覆盖。

  • 应对方案:同样可以使用 sync.Mutex 来同步追加操作。修改如下:
package main

import (
    "fmt"
    "sync"
)

var (
    s   = make([]int, 0, 5)
    mux sync.Mutex
)

func appendData() {
    for i := 0; i < 10; i++ {
        mux.Lock()
        s = append(s, i)
        mux.Unlock()
    }
}

func main() {
    for i := 0; i < 2; i++ {
        go appendData()
    }
    select {}
}
  • 最佳实践:预先分配足够的容量,减少扩容的频率,降低数据覆盖风险。同时,使用 channels 来安全地收集来自多个 goroutine 的数据,然后再进行统一处理。

3. 数组作为函数参数时的拷贝问题

  • 陷阱举例:Go 语言中数组作为函数参数是值传递,在并发环境下,如果函数对数组进行修改,可能无法影响到原数组。例如:
package main

import (
    "fmt"
)

func modifyArray(arr [5]int) {
    arr[0] = 100
}

func main() {
    var a [5]int
    go modifyArray(a)
    fmt.Println(a[0])
    select {}
}

这里 modifyArray 函数对数组的修改不会影响到主函数中的原数组 a

  • 应对方案:传递数组指针。修改后的代码如下:
package main

import (
    "fmt"
)

func modifyArray(arr *[5]int) {
    (*arr)[0] = 100
}

func main() {
    var a [5]int
    go modifyArray(&a)
    fmt.Println(a[0])
    select {}
}
  • 最佳实践:在并发编程中,如果需要对共享数组进行操作,优先考虑使用切片,因为切片本质是一个结构体,包含指向底层数组的指针,传递切片可以避免不必要的数组拷贝,提高效率。同时,要注意对切片操作的同步问题。