goでchannelやgoroutineを試すサンプルコードを書く時に便利なログ出力にする方法
標準のlogパッケージの使いかたをちょっと変えるだけで便利な形式で出力できそうなのでメモ。
:warning:まずはじめに大前提として、これはgoの挙動を確認するためのサンプルコードを書いてみるときの話であって、実際にアプリケーションなりツールなりを作ろうとしたときの話ではない。
(なので実際の開発でどのような形式のログ出力が良いかだとか、オススメのロガーの設定はというような話でもなく、何か外部のライブラリを使うと便利という話ではない)
準備
ちょっとだけlogパッケージの機能について触れる。既に分かっている人は読み飛ばしても良い。
defaultの出力
デフォルトでなにも考えずにlog出力をしてみたときには以下のような出力になる。
log.Println("hello")
出力結果
2018/12/20 06:59:38 hello
デフォルトでは以下の様な形式で出力される。
<date> <log message>
logに渡すオプション
よくあるログライブラリと同様に少しだけ出力内容を変えることができる。
ちなみにlog.Println()
という呼び出しはpackage内に保持したloggerを利用するショートカット。
log.go
var std = New(os.Stderr, "", LstdFlags) func Println(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) }
ここで LstdFlagsは Ldate | Ltime
の意味。実際に利用できるオプションに以下のようなものがある。
const ( Ldate = 1 << iota // the date in the local time zone: 2009/01/23 Ltime // the time in the local time zone: 01:23:23 Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime. Llongfile // full file name and line number: /a/b/c/d.go:23 Lshortfile // final file name element and line number: d.go:23. overrides Llongfile LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone LstdFlags = Ldate | Ltime // initial values for the standard logger )
幸い、log packageが保持したloggerのオプション設定を変えるショートカットも用意されている。
// SetFlags sets the output flags for the logger. func (l *Logger) SetFlags(flag int) { l.mu.Lock() defer l.mu.Unlock() l.flag = flag }
channelやgoroutineなどを使ったサンプルコード用の出力について
ここからが本題。実はちょっとした手間でchannelやgoroutineなどを利用するサンプルコードに適した表現に変えることができる。自分の理解のためのメモ書き程度のサンプルコードを書いて試してみるときに参考になるかもしれない。
ログ出力の分解能を上げて実行時間を短くする
デフォルトのログ出力では日付と時刻程度しかわからない。そして時刻の分解能が1秒単位なので、ついついサンプルコードに取り入れるwaittimeも1秒単位で指定してしまいがち。 例えば以下の様な形に。
package main import ( "log" "time" ) func main() { if err := run(); err != nil { log.Fatalf("%+v", err) } } func run() error { log.Printf("hello") time.Sleep(1 * time.Second) log.Printf("bye") return nil }
出力結果
$ time go run main.go 2018/12/20 07:14:50 hello 2018/12/20 07:14:51 bye real 0m1.198s user 0m0.191s sys 0m0.057s
ログ出力の分解能を上げてあげれば、わざわざ1秒待つ必要もない。例えば100ミリ秒にする。
--- 00stdlog/main.go 2018-12-20 07:12:04.519056801 +0900 +++ 01stdflags/main.go 2018-12-20 07:17:52.012278508 +0900 @@ -6,6 +6,7 @@ ) func main() { + log.SetFlags(log.Lmicroseconds) if err := run(); err != nil { log.Fatalf("%+v", err) } @@ -13,7 +14,7 @@ func run() error { log.Printf("hello") - time.Sleep(1 * time.Second) + time.Sleep(100 * time.Millisecond) log.Printf("bye") return nil }
実行時間を短くできる。
07:20:22.480065 hello 07:20:22.580553 bye real 0m0.287s user 0m0.181s sys 0m0.050s
メッセージの位置を表示する
ついでに出力の位置自体も表示してくれると嬉しい。先ほどと同様にオプションを追加する。
これはサンプルコードに限らずprint debugをしているときにも有用かもしれない。例えばわざわざメッセージを変えて出力していた部分があるとする(後々消すことになる)。
t := time.Now() fmt.Println(t, "start xxxx") doSomething() fmt.Println(t - time.Now(), "end xxxx")
この部分の記述にわざわざメッセージを変えるのが面倒に感じることがある(どうせ消すので)。最悪Lshortfile
を加えて、以下だけで済む。
log.Println("tick") doSomething() log.Println("tick") // 同じメッセージではあるけれど行数を確認できる
これを踏まえて、コードの位置も追加すると便利。
+++ 02with-shortfile/main.go 2018-12-20 07:26:41.252069947 +0900 @@ -6,7 +6,7 @@ ) func main() { - log.SetFlags(log.Lmicroseconds) + log.SetFlags(log.Lmicroseconds | log.Lshortfile) if err := run(); err != nil { log.Fatalf("%+v", err) }
出力結果
07:26:51.089306 main.go:16: hello 07:26:51.189609 main.go:18: bye
(おまけ) goroutine idも表示してみる
ついでに、goroutine idも一緒に表示できると便利かもしれない。例えば以下の様なgoroutineのコードを実行するときなどにどのgoroutineで動作しているか分かると理解のためには便利。
通常は自分でIDなどを付与してあげないとどのgoroutineが処理をしているかがわからないので(厳密に言えばどのgoroutineで動作しているかという話とコード上のどのタスク(頭の中)が処理しているかは別物ではあるけれど)。
var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() log.Println("hello") }() } wg.Wait()
出力結果
07:48:30.099717 main.go:21: hello 07:48:30.099696 main.go:21: hello 07:48:30.099740 main.go:21: hello 07:48:30.099749 main.go:21: hello 07:48:30.099745 main.go:21: hello
幸い、loggerは内部で利用しているio.Writerも変えられるので、以下の様なコードを追加することで、無理やりgoroutine idを付加させることもできなくはない。 (:warning: 内部的にはstopTheWorld()などが呼ばれているので注意。簡単にgoroutine idを取り出せない理由はhttps://qiita.com/ruiu/items/3574ac383f750bf95042)
type withGoroutineID struct { out io.Writer } func (w *withGoroutineID) Write(p []byte) (int, error) { // goroutine <id> [running]: firstline := []byte(strings.SplitN(string(debug.Stack()), "\n", 2)[0]) return w.out.Write(append(firstline[:len(firstline)-10], p...)) }
このwithGoroutineIDをsetOutput()で書き換えて使う。
diff -u 03goroutine/main.go 04withgoroutine/main.go --- 03goroutine/main.go 2018-12-20 07:48:23.878165771 +0900 +++ 04withgoroutine/main.go 2018-12-20 07:43:41.521625136 +0900 @@ -1,12 +1,26 @@ package main import ( + "io" "log" + "os" + "runtime/debug" + "strings" "sync" ) +type withGoroutineID struct { + out io.Writer +} + +func (w *withGoroutineID) Write(p []byte) (int, error) { + // goroutine <id> [running]: + firstline := []byte(strings.SplitN(string(debug.Stack()), "\n", 2)[0]) + return w.out.Write(append(firstline[:len(firstline)-10], p...)) +} + func main() { - log.SetFlags(log.Lmicroseconds | log.Lshortfile) + log.SetOutput(&withGoroutineID{out: os.Stderr}) if err := run(); err != nil { log.Fatalf("%+v", err) }
実行結果
goroutine 22 2018/12/20 07:53:18 hello goroutine 18 2018/12/20 07:53:18 hello goroutine 19 2018/12/20 07:53:18 hello goroutine 20 2018/12/20 07:53:18 hello goroutine 21 2018/12/20 07:53:18 hello
goroutine idが付加されている。
ついでに数が少ないなら色を変えてみても良いかもしれない。
最終的なコード
最終的なコードは以下。
package main import ( "io" "log" "os" "runtime/debug" "strings" "sync" ) type withGoroutineID struct { out io.Writer } func (w *withGoroutineID) Write(p []byte) (int, error) { // goroutine <id> [running]: firstline := []byte(strings.SplitN(string(debug.Stack()), "\n", 2)[0]) return w.out.Write(append(firstline[:len(firstline)-10], p...)) } func main() { log.SetOutput(&withGoroutineID{out: os.Stderr}) if err := run(); err != nil { log.Fatalf("%+v", err) } } func run() error { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() log.Println("hello") }() } wg.Wait() return nil }