延迟函数执行顺序

huangapple go评论82阅读模式
英文:

Defer function execution order

问题

我正在学习golang源代码,并在defer函数的执行顺序中遇到了困难。
我有两个文件:一个定义了端点的行为,另一个用于测试。为了减少需要阅读的行数,我删除了与问题无关的一些代码。
端点定义文件如下:

// Endpoint是服务器和客户端的基本构建块。
// 它表示一个RPC方法。
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)

// Middleware是端点的可链式行为修改器。
type Middleware func(Endpoint) Endpoint

// Chain是用于组合中间件的辅助函数。请求将按照声明的顺序遍历它们。也就是说,第一个中间件被视为最外层的中间件。
func Chain(outer Middleware, others ...Middleware) Middleware {
	return func(next Endpoint) Endpoint {
		for i := len(others) - 1; i >= 0; i-- { // reverse
			next = others[i](next)
		}
		return outer(next)
	}
}

测试文件包含打印的步骤。

func ExampleChain() {
	e := endpoint.Chain(
		annotate("first"),
		annotate("second"),
		annotate("third"),
	)(myEndpoint)

	if _, err := e(ctx, req); err != nil {
		panic(err)
	}

	// Output:
	// first pre
	// second pre
	// third pre
	// my endpoint!
	// third post
	// second post
	// first post
}

var (
	ctx = context.Background()
	req = struct{}{}
)

func annotate(s string) endpoint.Middleware {
	return func(next endpoint.Endpoint) endpoint.Endpoint {
		return func(ctx context.Context, request interface{}) (interface{}, error) {
			fmt.Println(s, "pre")
			defer fmt.Println(s, "post")
			return next(ctx, request)
		}
	}
}

func myEndpoint(context.Context, interface{}) (interface{}, error) {
	fmt.Println("my endpoint!")
	return struct{}{}, nil
}

根据我的理解,应该首先执行三个annotate方法,然后执行endpoint.Chain方法,最后执行myEndpoint。而且由于pre先打印,当函数返回时应该紧随其后打印post,根据Go文档中对defer的解释:

“defer”语句会调用一个函数,该函数的执行被推迟到包围函数返回的时刻,无论是因为包围函数执行了return语句,到达了函数体的结尾,还是因为相应的goroutine正在panic。

所以我期望看到的输出是:

// Output:
// first pre
// first post
// second pre
// second post
// third pre
// third post
// my endpoint!

简而言之,我的问题是:

  1. 为什么first pre后面没有跟着first post,同样的情况也发生在secondthird上。
  2. post的顺序被颠倒了。endpoint.Chain反转了一系列annotate返回值的执行顺序,但是annotate方法首先被评估,对吗?更不用说,pre被打印出来,这意味着内部函数首先被执行。
英文:

I am learning the golang source code and get stuck in the defer function execution order.
I have two files: one defines the behavior of an endpoint and another one for the test. I remove some code unrelated to my question to reduce the lines to read.
The endpoint definition file

// Endpoint is the fundamental building block of servers and clients.
// It represents a single RPC method.
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)

// Middleware is a chainable behavior modifier for endpoints.
type Middleware func(Endpoint) Endpoint

// Chain is a helper function for composing middlewares. Requests will
// traverse them in the order they're declared. That is, the first middleware
// is treated as the outermost middleware.
func Chain(outer Middleware, others ...Middleware) Middleware {
	return func(next Endpoint) Endpoint {
		for i := len(others) - 1; i >= 0; i-- { // reverse
			next = others[i](next)
		}
		return outer(next)
	}
}

The test file contains the printed steps.

func ExampleChain() {
	e := endpoint.Chain(
		annotate("first"),
		annotate("second"),
		annotate("third"),
	)(myEndpoint)

	if _, err := e(ctx, req); err != nil {
		panic(err)
	}

	// Output:
	// first pre
	// second pre
	// third pre
	// my endpoint!
	// third post
	// second post
	// first post
}

var (
	ctx = context.Background()
	req = struct{}{}
)

func annotate(s string) endpoint.Middleware {
	return func(next endpoint.Endpoint) endpoint.Endpoint {
		return func(ctx context.Context, request interface{}) (interface{}, error) {
			fmt.Println(s, "pre")
			defer fmt.Println(s, "post")
			return next(ctx, request)
		}
	}
}

func myEndpoint(context.Context, interface{}) (interface{}, error) {
	fmt.Println("my endpoint!")
	return struct{}{}, nil
}

To my understanding, the three annotate methods should be executed first, followed by the endpoint.Chain method and myEndpoint should be executed in the end. Also since the pre is printed first and when the funtion returns "post" should follow according to the defer explanation in the go doc:

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

So what I expect to see is

	// Output:
	// first pre
    // first post
	// second pre
	// second post
	// third pre
	// third post
	// my endpoint!

In short, my questions are:

  1. why first pre is not followed by first post, same as second third.
  2. the order of posts is reversed. the endpoint.Chain reverse the execution of a list of annotate returned values but annotate methods are evaluated first right? Not to say, the pres get printed which means the inner funtions are executed first

答案1

得分: 2

一个延迟函数在函数的最后一件事情之后运行,即在返回语句之后,所以annotate函数将首先运行next,只有在next返回之后延迟函数才会运行。根据你的代码,它应该按照以下顺序打印:

first pre
second pre
third pre
my endpoint
third post
second post
first post
英文:

A deferred function runs as the last thing in the function, after the return statement, so the annotate function will first run next, and only after that returns the deferred function will run. Based on your code, the order it should print is:

first pre
second pre
third pre
my endpoint
third post
second post
first post

答案2

得分: 2

以下是你的示例代码转换为在Go Playground上运行的代码:

请注意,如果在给定的函数中调用defer多次,每个延迟调用都按照LIFO(后进先出)的顺序执行。因此,如果你想使用defer确保post先被调用,然后再执行next,可以考虑将以下代码替换为:

defer fmt.Println(s, "post")
next(ctx, request)

改为:

defer next(ctx, request)
defer fmt.Println(s, "post")

当然,在你的情况下,你想要返回next的返回值,这会带来一个小问题。为了解决这个问题,你需要一个小函数和一些命名返回值:

defer func() { i, e = next(ctx, request) }()

其中ie是命名返回值。

这是相同的代码转换为一个新的示例,在该示例中延迟调用按照所需的顺序发生。 在这种情况下,示例有些愚蠢,因为没有发生恐慌,也没有在两个fmt.Println调用之间发生“危险步骤”,所以我们只需要按顺序执行这两个调用,而不使用defer。但是,如果我们可以fmt.Println(s, "pre")post部分之间发生恐慌,那么使用defer可能是有意义的。

英文:

Here is your example turned into something that runs on the Go playground.

Note that if you call defer more than once in a given function, each deferred call runs in LIFO order. So if you want to use defer to make sure your post gets called first, and then the next operates, consider replacing:

defer fmt.Println(s, "post")
next(ctx, request)

with:

defer next(ctx, request)
defer fmt.Println(s, "post)

Of course, in your case you want to return what next returns, which creates a small problem. To work around this in real cases you need a small function and some named return values:

defer func() { i, e = next(ctx, request) }()

where i and e are the named return values.

Here is that same code turned into a new example in which the deferred calls occur in the desired order. In this case, the example is rather silly, as nothing panics and there are no "dangerous steps" in between, so all we really need is to do the two fmt.Println calls sequentially, without using defer. But if we could panic between the fmt.Println(s, "pre") and the post section, then this might make sense.

huangapple
  • 本文由 发表于 2021年10月31日 10:47:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/69783597.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定