Goのdeferについて
$ go version
go version 1.12
この記事はGo: How Does defer Statement Work?の翻訳記事です。
defer
文は関数のreturn前にコードを実行する便利な手段です。
次のようにdefer
文はLIFO(last-in-first-out)のスタック実装になっています。
つまり、後に定義されたdefer
メソッド(defer
文で実行される関数)のほうが先に実行されます。
func main() {
defer func() {
println(`defer 1`)
}()
defer func() {
println(`defer 2`)
}()
}
defer 2 <- Last in, first to go out
defer 1
内部実装やもっと複雑なコードのときどうなるのかみていきましょう。
内部実装
GoはLIFOをリンクリストを使って実装しています。実際にdefer
構造体の中には次のdefer
構造体へのリンクを表すlink
フィールドが含まれています。
type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer // next deferred function to be executed
}
新しいdefer
メソッドが作られた時、それは現在のGoroutineにアタッチされ、前のdefer
メソッドを次に実行するようにリンクします。
func newdefer(siz int32) *_defer {
var d *_defer
gp := getg() // get the current goroutine
// ...
// deferred list is now attached to the new _defer struct
d.link = gp._defer
gp._defer = d // the new struct is now the first to be called
return d
}
defer
メソッドはスタックの上から順次実行されていきます。
func deferreturn(arg0 uintptr) {
gp := getg() // get the current goroutine
d:= gp._defer // copy the deferred function to a variable
if d == nil { // if there is not deferred func, just return
return
}
// ...
fn := d.fn // get the function to call
d.fn = nil // reset the function
gp._defer = d.link // attach the next one to the goroutine
freedefer(d) // freeing the _defer struc
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // call the func
}
// first deferred func
0x001d 00029 (main.go:6) MOVL $0, (SP)
0x0024 00036 (main.go:6) PCDATA $2, $1
0x0024 00036 (main.go:6) LEAQ "".main.func1·f(SB), AX
0x002b 00043 (main.go:6) PCDATA $2, $0
0x002b 00043 (main.go:6) MOVQ AX, 8(SP)
0x0030 00048 (main.go:6) CALL runtime.deferproc(SB)
0x0035 00053 (main.go:6) TESTL AX, AX
0x0037 00055 (main.go:6) JNE 117
// second deferred func
0x0039 00057 (main.go:10) MOVL $0, (SP)
0x0040 00064 (main.go:10) PCDATA $2, $1
0x0040 00064 (main.go:10) LEAQ "".main.func2·f(SB), AX
0x0047 00071 (main.go:10) PCDATA $2, $0
0x0047 00071 (main.go:10) MOVQ AX, 8(SP)
0x004c 00076 (main.go:10) CALL runtime.deferproc(SB)
0x0051 00081 (main.go:10) TESTL AX, AX
0x0053 00083 (main.go:10) JNE 101
// end of main func
0x0055 00085 (main.go:18) XCHGL AX, AX
0x0056 00086 (main.go:18) CALL runtime.deferreturn(SB)
0x005b 00091 (main.go:18) MOVQ 16(SP), BP
0x0060 00096 (main.go:18) ADDQ $24, SP
0x0064 00100 (main.go:18) RET
0x0065 00101 (main.go:10) XCHGL AX, AX
0x0066 00102 (main.go:10) CALL runtime.deferreturn(SB)
0x006b 00107 (main.go:10) MOVQ 16(SP), BP
0x0070 00112 (main.go:10) ADDQ $24, SP
0x0074 00116 (main.go:10) RET
deferproc
が各defer文でそれぞれ呼ばれています。これは先ほど紹介した新しくメソッドをdefer
メソッドとして登録する関数newdefer
を内部で呼び出しています。
その後、main
関数の終了時に、defer
メソッドがdeferreturn
関数によって次々と呼び出されていきます。
先ほど紹介した_defer
構造体には_panic *_panic
フィールドもありました。
これがどのような役割を果たすのか別の例を使ってみていきましょう。
deferメソッドの戻り値
deferメソッドの戻り値にアクセスする唯一の方法は、名前付き戻り値
を使うことです。
deferメソッドが関数リテラルであり、deferメソッドを含む関数がそのスコープ内に名前付き戻り値を持っている場合、その名前付き戻り値が返される前に、 deferメソッドは戻り値にアクセスして修正することができます。
これだけではわかりにくいので例とともにみていきましょう。
func main() {
fmt.Printf("with named param, x: %d\n", namedParam())
fmt.Printf("without named param, x: %d\n", notNamedParam())
}
func namedParam() (x int) {
x = 1
defer func() { x = 2 }()
return x
}
func notNamedParam() (int) {
x := 1
defer func() { x = 2 }()
return x
}
$ go run main.go
with named param, x: 2
without named param, x: 1
この挙動を知っていれば、recover関数への理解ももっと深まります。詳細はこちらの記事を参照してください。
_defer
構造体は_panic
フィールドを持っています。このフィールドはpanic時に設定されます。
func gopanic(e interface{}) {
[...]
var p _panic
[...]
d := gp._defer // current attached defer on the goroutine
[...]
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
[...]
}
実際にgopanic
はdeferメソッドを呼び出す前にpanicになった場合に呼び出されています。
0x0067 00103 (main.go:21) CALL runtime.gopanic(SB)
0x006c 00108 (main.go:21) UNDEF
0x006e 00110 (main.go:16) XCHGL AX, AX
0x006f 00111 (main.go:16) CALL runtime.deferreturn(SB)
実際に名前付き戻り値を使ってpanicからrecoverする関数の例を見てみましょう。
func main() {
fmt.Printf("error from err1: %v\n", err1())
fmt.Printf("error from err2: %v\n", err2())
}
func err1() error {
var err error
defer func() {
if r := recover(); r != nil {
err = errors.New("recovered")
}
}()
panic(`foo`)
return err
}
func err2() (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("recovered")
}
}()
panic(`foo`)
return err
}
error from err1: <nil>
error from err2: recovered
err2
ではerrが名前付き戻り値なためerr = errors.New("recovered")
が反映されます。
両方を組み合わせることで、recover関数と呼び出し元に返したいエラーを正しく動作させることができます。
パフォーマンスの改善
Go1.8でdefer
のパフォーマンスが改善されました。
試しに改善前(1.7)と改善後(1.8)でベンチマークを比較してみました。
name old time/op new time/op delta
Defer-4 99.0ns ± 9% 52.4ns ± 5% -47.04% (p=0.000 n=9+10)
Defer10-4 90.6ns ±13% 45.0ns ± 3% -50.37% (p=0.000 n=10+10)
50%近く速くなっています!
改善の内容について詳細を知りたい人はこちらを参照ください。
メモリコピーが行われない引数なしのdeferも最適化されています。
ここでは、引数なしでdefer文を実行した場合のベンチマークを紹介します。
name old time/op new time/op delta
Defer-4 51.3ns ± 3% 45.8ns ± 1% -10.72% (p=0.000 n=10+10)
こちらも10%ほど速くなっています。
Discussion