跳到主要内容

一些八股文

内存泄漏

内存泄漏指的是程序运行过程中已不再使用的内存,没有被释放掉,导致这些内存无法被使用,直到程序结束这些内存才被释放的问题。

Go 虽然有 GC 来回收不再使用的堆内存,减轻了开发人员对内存的管理负担,但这并不意味着 Go 程序不再有内存泄漏问题。

10 次内存泄漏,有 9 次是 goroutine 泄漏。

Goroutine 泄漏

Goroutine 泄漏是最常见最常见的内存泄漏了

如果你启动了 1 个 goroutine,但并没有符合预期的退出,直到程序结束,此 goroutine 才退出,这种情况就是 goroutine 泄漏。

如果 Goroutine 在执行时被阻塞而无法退出,就会导致 Goroutine 的内存泄漏,一个 Goroutine 的最低栈大小为 2KB,在高并发的场景下,对内存的消耗也是非常恐怖的。

互斥锁未释放

// 协程拿到锁未释放,其他协程获取锁会阻塞
func mutexTest() {
mutex := sync.Mutex{}
for i := 0; i < 10; i++ {
go func() {
mutex.Lock()
fmt.Printf("%d goroutine get mutex", i)
// 模拟实际开发中的操作耗时
time.Sleep(100 * time.Millisecond)
// mutex.UnLock()
}()
}
time.Sleep(10 * time.Second)
}

死锁

func mutexTest() {
m1, m2 := sync.Mutex{}, sync.RWMutex{}
// g1得到锁1去获取锁2
go func() {
m1.Lock()
fmt.Println("g1 get m1")
time.Sleep(1 * time.Second)
m2.Lock() // block
fmt.Println("g1 get m2")
}()
// g2得到锁2去获取锁1
go func() {
m2.Lock()
fmt.Println("g2 get m2")
time.Sleep(1 * time.Second)
m1.Lock() // block
fmt.Println("g2 get m1")
}()
// 其余协程获取锁都会失败
go func() {
m1.Lock() // block
fmt.Println("g3 get m1")
}()
time.Sleep(10 * time.Second)
}

空 channel

声明未初始化的 channel 读写都会阻塞

func channelTest() {
var c chan int // 未初始化
// 向channel中写数据
go func() {
c <- 1 // block
fmt.Println("g1 send succeed")
time.Sleep(1 * time.Second)
}()
// 从channel中读数据
go func() {
<-c // block
fmt.Println("g2 receive succeed")
time.Sleep(1 * time.Second)
}()
time.Sleep(10 * time.Second)
}

channel 写多于读

func channelTest() {
var c = make(chan int)
// 10个协程向channel中写数据
for i := 0; i < 10; i++ {
go func() {
c <- 1
fmt.Println("g1 send succeed")
time.Sleep(1 * time.Second)
}()
}
// 1个协程丛channel中读数据
go func() {
<-c
fmt.Println("g2 receive succeed")
time.Sleep(1 * time.Second)
}()
// 会有写的9个协程阻塞得不到释放
time.Sleep(10 * time.Second)
}

channel 读多于写

func channelTest() {
var c = make(chan int)
// 10个协程向channel中读数据
for i := 0; i < 10; i++ {
go func() {
<-c
fmt.Println("g1 receive succeed")
time.Sleep(1 * time.Second)
}()
}
// 1个协程丛channel写读数据
go func() {
c <- 1
fmt.Println("g2 send succeed")
time.Sleep(1 * time.Second)
}()
// 会有读的9个协程阻塞得不到释放
time.Sleep(10 * time.Second)
}

其他泄漏

获取长字符串中的一段导致长字符串未释放

var s0 string // a package-level variable

// A demo purpose function.
func f(s1 string) {
s0 = s1[:50]
// Now, s0 shares the same underlying memory block
// with s1. Although s1 is not alive now, but s0
// is still alive, so the memory block they share
// couldn't be collected, though there are only 50
// bytes used in the block and all other bytes in
// the block become unavailable.
}

解决方案

func f(s1 string) {
s0 = (" " + s1[:50])[1:]
}

获取长 slice 中的一段导致长 slice 未释放

异曲同工

var s0 []int

func g(s1 []int) {
// Assume the length of s1 is much larger than 30.
s0 = s1[len(s1)-30:]
}

func demo() {
s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
f(s)
}

解决方案:拷贝

func g(s1 []int) {
s0 = make([]int, 30)
copy(s0, s1[len(s1)-30:])
// Now, the memory block hosting the elements
// of s1 can be collected if no other values
// are referencing the memory block.
}

长 slice 新建 slice 导致泄漏

func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// do something with s ...

return s[1:3:3]
}

解决方案:

func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// do something with s ...

// Reset pointer values.
s[0], s[len(s)-1] = nil, nil
return s[1:3:3]
}

定时器泄漏

time.Ticker 每隔指定时间就会向通道内写数据。作为循环触发器,必须调用 stop 方法才会停止,从而被 GC 掉,否则会一直占用内存空间。

func tickerTest() {
// 定义一个ticker,每隔500毫秒触发
ticker := time.NewTicker(time.Second * 1)
// Ticker触发
go func() {
for t := range ticker.C {
fmt.Println("ticker被触发", t)
}
}()

time.Sleep(time.Second * 10)
// 停止ticker
ticker.Stop()
}