MST

星途 面试题库

面试题:Go语言defer在复杂嵌套与并发场景下的执行顺序探究

以下是一段复杂的Go语言代码: ```go package main import ( "fmt" "sync" ) func nestedFunction() { var wg sync.WaitGroup wg.Add(2) defer func() { fmt.Println("outer defer") wg.Wait() }() go func() { defer fmt.Println("inner go1 defer") defer func() { fmt.Println("inner go1 sub - defer") }() fmt.Println("inner go1") wg.Done() }() go func() { defer fmt.Println("inner go2 defer") defer func() { fmt.Println("inner go2 sub - defer") }() fmt.Println("inner go2") wg.Done() }() } func main() { nestedFunction() fmt.Println("main end") } ``` 1. 详细分析并描述该程序的输出顺序,包括不同defer在嵌套函数和并发goroutine中的执行逻辑。 2. 如果在 `outer defer` 中添加 `time.Sleep(time.Second)` 来模拟一些延迟操作,输出顺序会如何变化,为什么? 3. 如何修改代码,使得 `inner go1 sub - defer` 和 `inner go2 sub - defer` 在 `outer defer` 之前执行,同时保持整体逻辑的正确性?
37.2万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

1. 程序输出顺序及执行逻辑分析

  1. 主函数 main 开始执行:调用 nestedFunction
  2. 进入 nestedFunction
    • 初始化 sync.WaitGroup 并添加计数为2,准备等待两个goroutine完成。
    • 遇到外层 defer,将其压入延迟调用栈。此时该 defer 不会立即执行,而是在 nestedFunction 函数结束时执行。其逻辑是打印 "outer defer" 并等待两个goroutine完成(通过 wg.Wait())。
    • 启动第一个goroutine:
      • 进入该goroutine,将第一个 defer(打印 "inner go1 defer")压入延迟调用栈。
      • 再将内层的 defer(打印 "inner go1 sub - defer")压入延迟调用栈。
      • 打印 "inner go1"
      • 调用 wg.Done(),减少 WaitGroup 的计数。
      • 该goroutine结束,按照延迟调用栈的顺序,先打印 "inner go1 sub - defer",再打印 "inner go1 defer"
    • 启动第二个goroutine:
      • 进入该goroutine,将第一个 defer(打印 "inner go2 defer")压入延迟调用栈。
      • 再将内层的 defer(打印 "inner go2 sub - defer")压入延迟调用栈。
      • 打印 "inner go2"
      • 调用 wg.Done(),减少 WaitGroup 的计数。
      • 该goroutine结束,按照延迟调用栈的顺序,先打印 "inner go2 sub - defer",再打印 "inner go2 defer"
    • nestedFunction 函数执行完毕,执行外层 defer,打印 "outer defer" 并等待两个goroutine完成(此时两个goroutine已完成,wg.Wait() 立即返回)。
  3. 回到 main 函数:打印 "main end"

所以,正常输出顺序为:

inner go1
inner go2
inner go1 sub - defer
inner go2 sub - defer
inner go1 defer
inner go2 defer
outer defer
main end

2. 在外层 defer 中添加 time.Sleep(time.Second) 后的输出顺序变化

  1. 主函数 main 开始执行:调用 nestedFunction
  2. 进入 nestedFunction
    • 初始化 sync.WaitGroup 并添加计数为2,准备等待两个goroutine完成。
    • 遇到外层 defer,将其压入延迟调用栈。
    • 启动第一个goroutine:
      • 进入该goroutine,将第一个 defer(打印 "inner go1 defer")压入延迟调用栈。
      • 再将内层的 defer(打印 "inner go1 sub - defer")压入延迟调用栈。
      • 打印 "inner go1"
      • 调用 wg.Done(),减少 WaitGroup 的计数。
      • 该goroutine结束,按照延迟调用栈的顺序,先打印 "inner go1 sub - defer",再打印 "inner go1 defer"
    • 启动第二个goroutine:
      • 进入该goroutine,将第一个 defer(打印 "inner go2 defer")压入延迟调用栈。
      • 再将内层的 defer(打印 "inner go2 sub - defer")压入延迟调用栈。
      • 打印 "inner go2"
      • 调用 wg.Done(),减少 WaitGroup 的计数。
      • 该goroutine结束,按照延迟调用栈的顺序,先打印 "inner go2 sub - defer",再打印 "inner go2 defer"
    • nestedFunction 函数执行完毕,执行外层 defer,先睡眠1秒,然后打印 "outer defer" 并等待两个goroutine完成(此时两个goroutine已完成,wg.Wait() 立即返回)。
  3. 回到 main 函数:打印 "main end"

输出顺序变为:

inner go1
inner go2
inner go1 sub - defer
inner go2 sub - defer
inner go1 defer
inner go2 defer
// 等待1秒
outer defer
main end

原因是 time.Sleep(time.Second) 会阻塞当前goroutine(这里是执行外层 defer 的goroutine)1秒钟,所以在两个goroutine完成后会等待1秒再打印 "outer defer"

3. 修改代码使 inner go1 sub - deferinner go2 sub - deferouter defer 之前执行

可以通过使用通道(channel)来同步goroutine。修改后的代码如下:

package main

import (
    "fmt"
    "sync"
    "time"
)

func nestedFunction() {
    var wg sync.WaitGroup
    wg.Add(2)
    var subDeferChan = make(chan struct{})

    go func() {
        defer fmt.Println("inner go1 defer")
        defer func() {
            fmt.Println("inner go1 sub - defer")
            subDeferChan <- struct{}{}
        }()
        fmt.Println("inner go1")
        wg.Done()
    }()

    go func() {
        defer fmt.Println("inner go2 defer")
        defer func() {
            fmt.Println("inner go2 sub - defer")
            subDeferChan <- struct{}{}
        }()
        fmt.Println("inner go2")
        wg.Done()
    }()

    go func() {
        wg.Wait()
        close(subDeferChan)
    }()

    defer func() {
        fmt.Println("outer defer")
    }()

    for i := 0; i < 2; i++ {
        <-subDeferChan
    }
}

func main() {
    nestedFunction()
    fmt.Println("main end")
}
  1. 添加通道 subDeferChan:用于同步内层子 defer 的执行。
  2. 在每个内层子 defer:向通道发送信号。
  3. 添加一个新的goroutine:等待两个goroutine完成(通过 wg.Wait())后关闭通道。
  4. 在外层 defer 之前:通过循环从通道接收两个信号,确保内层子 defer 都执行完毕。这样就可以保证 inner go1 sub - deferinner go2 sub - deferouter defer 之前执行,同时保持整体逻辑的正确性。