延迟执行和命名返回值是如何工作的?

64

我刚开始学习Go语言,看到The Go Blog - Defer, Panic, and Recover的一个例子中使用defer来改变有名返回值,让我产生了困惑。

这个例子说:

  1. 延迟函数可以读取和赋值给返回函数的命名返回值。

在这个例子中,一个被延迟的函数会在包裹函数返回后将返回值 i 加1。因此,这个函数返回 2

func c() (i int) {
    defer func() { i++ }()
    return 1
}

但是根据我从Go之旅-命名返回值学到的知识:

没有参数的返回语句返回带有名称的返回值。 这被称为“裸”的返回。

我在以下代码中进行了测试,并且在函数b中返回1,因为它不是上面提到的“没有参数的返回语句”的情况。

func a() (i int) { // return 2
    i = 2
    return
}

func b() (i int) {  // return 1 
    i = 2
    return 1
}

所以我的问题是,在第一个例子中,包围函数c具有命名返回值i,但函数 c 使用return 1,在第二个例子中,无论i 的值是什么,它都应该返回1。但是为什么在延迟函数更改 i 的值后, c 函数返回 i 的值而不是1?

当我输入问题时,我可能已经猜到答案了。那是因为:

return 1 

等于:

i = 1
return 
在一个具有命名返回值变量i的函数中?
请帮我确认,谢谢!
5个回答

61
一个defer语句会将一个函数调用推入到列表中。保存的调用列表会在周围函数返回后执行。-- The Go Blog: Defer, Panic, and Recover 理解上述语句的另一种方式是:
一个defer语句会将一个函数调用推入堆栈中。保存的调用堆栈弹出(LIFO),延迟的函数会在周围函数返回之前立即被调用。
 func c() (i int) {
    defer func() { i++ }()
    return 1
}

在返回1后,延迟执行func() { i++ }()。因此,执行顺序为:

  1. i = 1(返回1)
  2. i++(延迟函数从堆栈中弹出并执行)
  3. i == 2(命名变量i的最终结果)

为了理解的目的:
 func c() (i int) {
    defer func() { fmt.Println("third") }()
    defer func() { fmt.Println("second") }()
    defer func() { fmt.Println("first") }()

    return 1
}

执行顺序:

  1. i = 1(返回1)
  2. "first"
  3. "second"
  4. "third"

2
谢谢,我明白defer的LIFO原则了,其实我一直不太理解有名返回值,现在我清楚了! - Hammer
13
“在函数返回后执行”的翻译是不正确的。规范非常明确地说明“延迟函数在其所属函数即将返回之前立即被调用”,而不是在之后。如果延迟函数在返回之后被调用,则无法在func c的情况下更改返回值。因此,执行顺序应为:i = 1(赋值)、i++(延迟执行递增)、return,返回值为2。 - Elias Van Ootegem
1
@EliasVanOotegem 已修复! :) - Roy Lee
这是一个有趣的Go语言“陷阱”之一,让我措手不及。 - simplytunde
1
OP询问返回值为2的原因,我在阅读文档时也感到困惑。defer的“后进先出”特性已经很清楚了。如果你的回答更关注返回值,那就太好了。谢谢。Go真是一个不错的语言。 - Long

12
根据Go规范:

返回语句 在任何延迟函数执行之前,指定结果的"return"语句将设置结果参数。

延迟语句 "...延迟函数会在周围函数返回之前立即被调用..."

因此,正如您所认为的那样,命名的返回变量被赋值,然后延迟语句对其进行递增。

我想补充说明,命名返回参数可能会导致微妙的bug,并且通常应该避免使用,除非没有其他选择。


7

我想混淆的是如何在函数中进行函数的问题,如果您按照以下方式分类,会产生什么影响:

  func main() {
      fmt.Println(c()) //the result is 5
  }

  // the c function returned value is named j
  func c() (j int)  {
      defer changei(&j)
      return 6
  }
  func changei(j *int) {
      //now j is 6 because it was assigned by return statement 
      // and if i change guess what?! i changed the returned value
      *j--;
  }

但是如果返回值的名称不像这样命名:
  func main() {
      fmt.Println(c()) //the result will become 6
  }

  // the c function returned value is not named at this time
  func c() int  {
      j := 1
      defer changei(&j)
      return 6
  }
  func changei(j *int) {
      //now j = 1
      // and if i change guess what?! it will not effects the returned value
      *j--;
  }

我希望这能够消除困惑并展示我如何快乐地进行Go编程


我猜你的意思是这个:https://play.golang.org/p/5zBeF9hoOCn。你必须在c()函数中返回j。 - beyondfloatingpoint


0
也许这个函数的返回值被命名了。
func c() (i int) {
  defer func() { i++ }()
  return 1
}

将会像这样编译:
func c() int {
  defer func() { i++ }()
  i = 1
  return i
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接