MST

星途 面试题库

面试题:Go实参传递性能优化之切片和map

在Go语言中,切片和map作为函数实参传递时,其内部的传递机制是怎样的?如何针对它们的传递特性进行性能优化?例如在高并发场景下,对切片和map传递时可能会遇到什么性能问题,如何解决?
50.3万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

切片和map作为函数实参的传递机制

  1. 切片
    • 在Go语言中,切片是一个引用类型。当切片作为函数实参传递时,传递的是切片的指针(虽然Go语言中没有指针操作符*来显式表示),本质上传递的是一个包含指向底层数组的指针、切片长度和容量的结构体。
    • 例如:
package main

import "fmt"

func modifySlice(s []int) {
    s[0] = 100
}

func main() {
    mySlice := []int{1, 2, 3}
    modifySlice(mySlice)
    fmt.Println(mySlice)
}
  • 在上述代码中,modifySlice函数中对切片的修改会反映到原切片上,因为传递的是指向底层数组的指针等信息。
  1. map
    • map同样是引用类型。当map作为函数实参传递时,传递的是map的引用,即指向map数据结构的指针。
    • 例如:
package main

import "fmt"

func modifyMap(m map[string]int) {
    m["key"] = 200
}

func main() {
    myMap := make(map[string]int)
    myMap["key"] = 100
    modifyMap(myMap)
    fmt.Println(myMap["key"])
}
  • 这里modifyMap函数对map的修改也会体现在原map上,因为传递的是map的引用。

性能优化

  1. 切片
    • 预分配内存:在创建切片时,如果能提前预估切片的大小,使用make函数预分配足够的内存可以避免在添加元素时频繁的内存重新分配。例如mySlice := make([]int, 0, 100),这里预先分配了容纳100个int类型元素的内存,后续添加元素时只要不超过容量就不会重新分配内存。
    • 减少不必要的复制:由于切片传递的是引用,在函数内部对切片操作尽量避免创建新的切片(如果不需要新的独立数据结构),以免增加内存开销。
  2. map
    • 预分配内存:和切片类似,创建map时可以预分配内存。例如myMap := make(map[string]int, 100),这样可以减少在添加元素时哈希表重新分配内存和重新哈希的开销。
    • 合理选择键类型:选择合适的键类型,避免使用大的结构体作为键,因为键在哈希计算和比较时会有额外开销。尽量使用基本类型(如intstring)作为键。

高并发场景下的性能问题及解决

  1. 切片
    • 性能问题:高并发场景下,多个goroutine同时读写切片可能会导致数据竞争问题,例如一个goroutine在读取切片元素时,另一个goroutine同时修改切片长度或底层数组,可能导致程序崩溃或数据不一致。
    • 解决方法
      • 互斥锁:使用sync.Mutex来保护对切片的读写操作。例如:
package main

import (
    "fmt"
    "sync"
)

var mu sync.Mutex
var sharedSlice []int

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

func readSlice(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    fmt.Println(sharedSlice)
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go writeSlice(&wg)
    }
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go readSlice(&wg)
    }
    wg.Wait()
}
 - **读写锁**:如果读操作远多于写操作,可以使用`sync.RWMutex`,读操作使用读锁,写操作使用写锁,以提高并发性能。

2. map

  • 性能问题:在高并发场景下,多个goroutine同时读写map同样会导致数据竞争问题,Go语言的map本身不是线程安全的,直接在高并发下读写会导致程序崩溃。
  • 解决方法
    • sync.Map:Go 1.9引入了sync.Map,它是线程安全的map实现,适合在高并发场景下使用。例如:
package main

import (
    "fmt"
    "sync"
)

var sharedMap sync.Map

func writeMap(wg *sync.WaitGroup) {
    defer wg.Done()
    sharedMap.Store("key", 1)
}

func readMap(wg *sync.WaitGroup) {
    defer wg.Done()
    value, ok := sharedMap.Load("key")
    if ok {
        fmt.Println(value)
    }
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go writeMap(&wg)
    }
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go readMap(&wg)
    }
    wg.Wait()
}
 - **使用互斥锁**:和切片类似,也可以使用`sync.Mutex`来保护对map的操作,但`sync.Map`在高并发下性能通常更好。