どう違うか

結論からいうと、以下のような違いがあります。

  • log.Fatal: os.Exit(1) を呼び出して強制終了。defer などは呼ばれず、その場でプログラムが終了する。
  • t.Fatal : runtime.Goexit() を呼び出して終了。defer 処理を行い、その goroutine を終了。

最後に呼び出しているプログラムが違うわけですから、全く違う挙動になるのは当たり前なんですが、いかんせん名前が紛らわしいですよね。。。

詳しい挙動

次にそれぞれの詳しい挙動を確認してきます。

log.Fatal

この関数で呼ばれる os.Exitドキュメントは以下です。

Exit causes the current program to exit with the given status code. Conventionally, code zero indicates success, non-zero an error. The program terminates immediately; deferred functions are not run. For portability, the status code should be in the range [0, 125].

defer も呼ばれないでその場でプログラムを終了すると書いてあります。なので defer などを使わない main 関数ぐらいでしか使いみちがないですね。 一応挙動が分かりやすいように以下のようなコードを書いてみました。

package main

import (
	"log"
)

func main() {
	defer log.Print("end")
	c := make(chan struct{}, 1)
	go func() {
		defer func() {
			log.Print("subprocess end")
			c <- struct{}{}
		}()
		log.Fatal("exit")
	}()
	<-c
	log.Printf("last code")
}

これの出力は以下のようになります。本当に何もせずその場でプログラムが強制終了していることが分かると思います。

2020/10/15 13:49:34 exit
exit status 1

t.Fatal

次にこちらの関数で呼ばれるruntime.Goexitドキュメントを見ていきます。

Goexit terminates the goroutine that calls it. No other goroutine is affected. Goexit runs all deferred calls before terminating the goroutine. Because Goexit is not a panic, any recover calls in those deferred functions will return nil. Calling Goexit from the main goroutine terminates that goroutine without func main returning. Since func main has not returned, the program continues execution of other goroutines. If all other goroutines exit, the program crashes.

defer を呼び出した後にその goroutine を終了するだけで、他の goroutine には影響がでないことが書いてあります。ちょっと確認してみます。

以下のようなコードで defer を呼び出した goroutine のみが終了していることを確認できます。

func TestRoutineExit(t *testing.T) {
	defer t.Log("defer end")
	c := make(chan struct{}, 1)
	go func() {
		defer func() {
			c <- struct{}{}
		}()
		t.Fatal("exit go routine")
	}()
	<-c
	t.Log("process end")
}

結果は以下のようになります。main スレッドに影響が出ていないことがよく分かります。

--- FAIL: TestRoutineExit (0.00s)
    exit_test.go:20: exit go routine
    exit_test.go:23: process end
    exit_test.go:24: defer end
FAIL

因みに上に記したコードと似たような処理が t.Run で行われています。そのため、t.Runでも似たような結果になります。

func TestSubExit(t *testing.T) {
	for i := 0; i < 2; i++ {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Fatalf("%d sub program exit", i)
		})
	}
	t.Log("end")
}
--- FAIL: TestSubExit (0.00s)
    --- FAIL: TestSubExit/0 (0.00s)
        exit_test.go:29: 0 sub program exit
    --- FAIL: TestSubExit/1 (0.00s)
        exit_test.go:29: 1 sub program exit
    exit_test.go:32: end
FAIL

最後に、main プロセスでt.Fatalを呼んだ場合は、どうなるか確認します。一応ドキュメントには defer 処理を行った後にクラッシュすると書いてありますが、実際の挙動も見ていきます。

func TestExit(t *testing.T) {
	defer t.Log("end")
	go func() {
		t.Log("goroutine")
	}()
	t.Fatal("exit program")
}

上記のプログラムの実行結果は以下になります。

--- FAIL: TestExit (0.00s)
    exit_test.go:13: exit program
    panic.go:617: end
FAIL

defer のコードが panic として呼ばれていますね。panic になるため、defer コードは呼ばれていますが、メインの goroutine なのか、子供の goroutine なのかで挙動が変わることがよく分かります。

同様にt.Runでも同じ現象になります。t.Runがサブプロセスで動いていることがよく分かりますが、気をつけないと嵌りそうな挙動です。

func TestSubMainExit(t *testing.T) {
	defer t.Log("end")
	t.Run("sub process", func(t *testing.T) {
		t.Log("goroutine")
	})
	t.Fatal("exit program")
}
--- FAIL: TestSubMainExit (0.00s)
    exit_test.go:21: exit program
    panic.go:617: end

小ネタ的な話ですが、以下のようなコードの場合は、goroutine が終わるまでにテストが全て終了すれば正常。メインのテスト中に終われば異常終了。他のテスト中に goroutine が終われば panic とメチャクチャな挙動を味わえます。 こういうコードを書いては行けないというのをしみじみと感じます。

func TestRoutineNoWatiExit(t *testing.T) {
	defer t.Log("defer end")
	go func() {
		t.Fatal("exit go routine")
	}()
	t.Log("process end")
}

t.Fatal の確認で使ったコードはこちらから試すことが出来ます

まとめ

log.Fatal と t.Fatal の違いを見ていきました。内部関数が違うために挙動が全く違うことが分かったと思います。 同じことを二度書くことになりますが、結局は最初にまとめたところに落ち着きます。

  • log.Fatal: os.Exit(1) を呼び出して強制終了。defer などは呼ばれず、その場でプログラムが終了する。
  • t.Fatal : runtime.Goexit() を呼び出して終了。defer 処理を行い、その goroutine を終了。

挙動を意識してコードを書かないとなぁって思います。