关于 Go 的一些八股文

一些八股文

内存泄漏

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

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

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

Goroutine 泄漏

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

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

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

互斥锁未释放

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 协程拿到锁未释放,其他协程获取锁会阻塞
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)
}

死锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 读写都会阻塞

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 写多于读

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 读多于写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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)
}

其他泄漏

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.
}

解决方案

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

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

异曲同工

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)
}

解决方案:拷贝

1
2
3
4
5
6
7
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 导致泄漏

1
2
3
4
5
6
func h() []*int {
	s := []*int{new(int), new(int), new(int), new(int)}
	// do something with s ...

	return s[1:3:3]
}

解决方案:

1
2
3
4
5
6
7
8
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 掉,否则会一直占用内存空间。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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()
}