关于golang:Go-Quiz-从Go面试题看defer语义的底层原理和注意事项

38次阅读

共计 3313 个字符,预计需要花费 9 分钟才能阅读完成。

面试题

这是 Go Quiz 系列的第 4 篇,对于 Go 语言的 defer 语义。

这道题略微有点迷惑性,通过这道题能够加深咱们对 Go 语言 defer 关键字底层运行机制的了解。

package main

type Foo struct {v int}

func NewFoo(n *int) Foo {print(*n)
    return Foo{}}

func (Foo) Bar(n *int) {print(*n)
}

func main() {
    var x = 1
    var p = &x
    defer NewFoo(p).Bar(p)
    x = 2
    p = new(int)
    NewFoo(p)
}
  • A: 100
  • B: 102
  • C: 022
  • D: 011

这道题次要考查以下知识点:

  • defer 的函数或办法什么时候执行?
  • defer 的函数或办法的参数的值是什么时候确定的?
  • defer 的函数或办法如果存在多级调用是什么机制?比方本题的 NewFoo(p).Bar(p) 就存在二级调用,先调用了 NewFoo 函数,再调用了 Bar 办法。

解析

咱们再看看官网文档怎么说的:

Each time a “defer” statement executes, the function value and parameters to
the call are evaluated as usual and saved anew but the actual function is not
invoked.

Instead, deferred functions are invoked immediately before the
surrounding function returns, in the reverse order they were deferred.

That is, if the surrounding function returns through an explicit return statement,
deferred functions are executed after any result parameters are set by that
return statement but before the function returns to its caller.

If a deferred function value evaluates to nil, execution panics when the function is
invoked, not when the “defer” statement is executed.

官网文档的前两句话对咱们求解本题至关重要,用中文来表述就是:

假如咱们在函数 A 的函数体内运行了 defer B(params),那被defer 的函数 B 的参数会像一般函数调用一样被计算,然而被 defer 的函数 B 的调用会提早到函数A return 或者 panic 之前执行。

如果 defer 前面跟的是多级函数的调用,只有最初一个函数会被提早执行。

比方上例里的 defer NewFoo(p).Bar(p) 中,NewFoo(p)是会被即时执行的,不会延后,只有最初一个办法 Bar 的调用会被延后执行。

同时,函数的传参也是被即时计算的,也就是 defer NewFoo(p).Bar(p) 里波及的参数 p 的值也是被即时计算保留好的,延后执行的时候就用当时计算好的值。

这道题求解过程如下:

代码 执行后果
var x = 1 定义一个整型变量 x,值为 1
var p = &x 定义一个指针 p,指向变量 x
defer NewFoo(p).Bar(p) NewFoo(p)和参数 p 会被即时计算。NewFoo(p)的执行后果会打印 1,指针 p 的值是变量 x 的内存地址,* p 也就是变量 x 的值,Bar 的调用会延后到 main 函数 return 之前执行
x = 2 批改 x 的值为 2
p = new(int) 批改指针 p 的值,不再指向 x,而是指向新的内存地址,该内存地址存的值是 int 的零值,也就是 0
NewFoo(p) 调用函数 NewFoo,打印 0。
main 函数 return 之前,执行 Bar 办法,Bar 的参数 p 的值在 defer 语句执行的时候就曾经确定下来了,是变量 x 的内存地址,因而 Bar 办法打印的是变量 x 的值,也就是 2

因而本题的运行后果是 102,答案是 B。

defer 六大准则

最初总结下 defer 语义要留神的六大关键点:

  1. defer 前面跟的必须是函数或者办法调用,defer 前面的表达式不能加括号。

    defer (fmt.Println(1)) // 编译报错,因为 defer 前面跟的表达式不能加括号
  2. 被 defer 的函数或办法的参数的值在执行到 defer 语句的时候就被确定下来了。

    func a() {
        i := 0
        defer fmt.Println(i) // 最终打印 0
        i++
        return
    }

    上例中,被 defer 的函数 fmt.Println 的参数 i 在执行到 defer 这一行的时候,i的值是 0,fmt.Println 的参数就被确定下来是 0 了,因而最终打印的后果是 0,而不是 1。

  3. defer 的函数或者办法如果存在多级调用,只有最初一个函数或办法会被 defer 到函数 return 或者 panic 之前执行,参见下面的阐明。
  4. 被 defer 的函数执行程序满足 LIFO 准则,后 defer 的先执行。

    func b() {
        for i := 0; i < 4; i++ {defer fmt.Print(i)
        }
    }

    上例中,输入的后果是 3210,后 defer 的先执行。

  5. 被 defer 的函数能够对 defer 语句所在的函数的命名返回值做读取和批改操作。

    // f returns 42
    func f() (result int) {defer func() {
            // result is accessed after it was set to 6 by the return statement
            result *= 7
        }()
        return 6
    }

    上例中,被 defer 的函数 func 对 defer 语句所在的函数 f 的命名返回值 result 做了批改操作。

    调用函数f,返回的后果是 42。

    执行程序是函数 f 先把要返回的值 6 赋值给 result,而后执行被 defer 的函数 func,result 被批改为 42,而后函数 f 返回 result 给调用方,也就返回了 42。

  6. 即便 defer 语句执行了,被 defer 的函数不肯定会执行。对这句话不了解的能够参考上面的思考题。

思考题

思考上面这 3 道题的运行后果是什么?大家能够在评论区留下你们的答案。

题目 1:程序运行后果是什么?

package main

type T int

func (t T) M(n int) T {print(n)
    return t
}

func main() {
    var t T
    defer t.M(1).M(2)
    t.M(3).M(4)
}

题目 2:”end” 会被打印么?f(1, 2)会不会编译报错?

package main

import "fmt"

type Add func(int, int) int

func main() {
    var f Add
    defer f(1, 2)
    fmt.Println("end")
}

题目 3:“test”会被打印么?

package main

import (
    "fmt"
    "os"
)

func test1() {fmt.Println("test")
}

func main() {defer test1()
    os.Exit(0)
}

开源地址

文章和示例代码开源地址在 GitHub: https://github.com/jincheng9/go-tutorial

公众号:coding 进阶

集体网站:https://jincheng9.github.io/

知乎:https://www.zhihu.com/people/thucuhkwuji

好文举荐

  1. 被 defer 的函数肯定会执行么?
  2. Go 有援用变量和援用传递么?map,channel 和 slice 作为函数参数是援用传递么?
  3. new 和 make 的应用区别是什么?
  4. 一文读懂 Go 匿名构造体的应用场景
  5. 官网教程:Go 泛型入门
  6. 一文读懂 Go 泛型设计和应用场景
  7. Go Quiz: 从 Go 面试题看 slice 的底层原理和注意事项
  8. Go Quiz: 从 Go 面试题看 channel 的注意事项
  9. Go Quiz: 从 Go 面试题看分号规定和 switch 的注意事项

References

  • https://go101.org/quizzes/def…
  • https://github.com/jincheng9/…
  • https://golang.google.cn/ref/…

正文完
 0