MST

星途 面试题库

面试题:在高并发场景下Go语言数组与切片的选择权衡

在高并发的Go语言程序中,有时需要处理共享数据。请分析在这种场景下,数组与切片各自的优缺点,如何根据具体的业务需求(如数据读写频率、数据一致性要求等)来选择使用数组还是切片?请结合实际代码示例说明如何通过适当的同步机制(如互斥锁、读写锁等)来保证数据的正确访问。
33.9万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

数组与切片在高并发共享数据场景下的优缺点分析

  1. 数组
    • 优点
      • 数组的长度是固定的,在编译时就确定,内存布局连续,对于固定大小的数据集合,使用数组访问效率较高,因为可以通过简单的偏移量快速定位元素。例如在一些固定长度的配置数据存储场景下,数组能高效存储和访问。
    • 缺点
      • 长度固定,一旦声明大小不能改变。在高并发且数据量动态变化的场景下,数组灵活性不足。如果预估数据量不准确,可能导致空间浪费或者数据溢出。
  2. 切片
    • 优点
      • 切片长度可变,能根据需求动态增长和收缩,非常适合数据量不确定的场景。例如在处理高并发的日志记录,随着日志不断产生,切片可以动态扩展存储。
      • 切片是引用类型,在函数传递等操作时,开销较小,因为传递的是切片结构体(包含指向底层数组的指针、长度和容量),而不是整个数据副本。
    • 缺点
      • 由于切片长度可变,其内存管理相对复杂,频繁的扩容操作可能带来性能开销。在高并发下,扩容操作可能会导致额外的资源竞争。

根据业务需求选择数组或切片

  1. 数据读写频率
    • 读写频率低:如果数据读写频率较低且数据量固定,数组是较好的选择,因其固定长度和简单的内存布局能提供稳定的访问性能。例如,程序启动时读取一次配置数据并在后续少量使用,数组可有效存储配置数据。
    • 读写频率高:当读写频率高且数据量动态变化时,切片更合适。如实时统计高并发请求的相关数据,随着请求不断到来,切片可动态扩展存储统计信息。
  2. 数据一致性要求
    • 强一致性要求:对于数据一致性要求极高的场景,数组和切片都需要配合合适的同步机制。但数组相对简单的结构在使用同步机制时可能更容易理解和实现。例如银行账户余额等关键数据存储,使用数组并结合同步机制确保数据一致性。
    • 弱一致性要求:在一些对一致性要求不那么严格的场景,如统计网站实时在线人数,切片的动态扩展性更能满足业务需求,通过适当的同步机制也能保证一定程度的数据正确性。

结合同步机制保证数据正确访问的代码示例

  1. 使用互斥锁(sync.Mutex)示例
package main

import (
    "fmt"
    "sync"
)

var (
    mu    sync.Mutex
    slice []int
    array [5]int
)

func writeSlice(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    slice = append(slice, 1)
    mu.Unlock()
}

func writeArray(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    for i := range array {
        array[i]++
    }
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go writeSlice(&wg)
    }
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go writeArray(&wg)
    }
    wg.Wait()
    fmt.Println("Slice:", slice)
    fmt.Println("Array:", array)
}

在这个示例中,无论是对切片还是数组进行写操作,都通过互斥锁mu来保证同一时间只有一个 goroutine 能进行写操作,从而保证数据的一致性。 2. 使用读写锁(sync.RWMutex)示例

package main

import (
    "fmt"
    "sync"
)

var (
    rwmu    sync.RWMutex
    slice   []int
    array   [5]int
)

func readSlice(wg *sync.WaitGroup) {
    defer wg.Done()
    rwmu.RLock()
    fmt.Println("Reading slice:", slice)
    rwmu.RUnlock()
}

func writeSlice(wg *sync.WaitGroup) {
    defer wg.Done()
    rwmu.Lock()
    slice = append(slice, 1)
    rwmu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go readSlice(&wg)
    }
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go writeSlice(&wg)
    }
    wg.Wait()
}

这里使用读写锁rwmu,读操作使用读锁RLock,允许多个 goroutine 同时读;写操作使用写锁Lock,保证写操作时其他 goroutine 不能读写,以此保证高并发下数据的正确访问。