Golang - logパッケージで出力を実行時に抑止する

             

Go の 標準 log パッケージ からの出力を実行時に抑止する方法について

モチベーション

CLI ツール を作っている際に、 所謂 verbose モード を提供したいと思った。

OPTIONS:
-verbose 詳細メッセージを出力する

各種CLIツールでよくお目にかかるオプションかと思う。

各種ロギングライブラリは、大抵実行時に出力するログレベルを制御する機能が提供されているため、 main.go だけで完結する様な 簡易なツールである場合は それらを導入してしまえば実現は簡単だ。

しかし、今回作ろうとしている ツール は基本的な処理はライブラリとして単独で提供し、 エントリーポイントとして main パッケージを実装する構成にしていたため、既存のロギングライブラリ の採用はリスクが伴う。

既存のロギングライブラリを使うのは絶対に止めましょう。 あなたのライブラリの機能を使いたいだけなのに特定のロギングライブラリを強制されるのは、 ライブラリのユーザーにとっては苦痛でしかありません。 Golangでログを吐くコツ — KaoriYa

では、標準 log パッケージ を使ってどの様に解決するか?

解決方法

main パッケージにて log.SetOutput に対して io/ioutil.Discard を指定することで log.Println の出力を抑止することができる。

package main

import (
	"io/ioutil"
	"log"
	"os"
)

func main() {
	log.SetOutput(ioutil.Discard)
	log.Println("This will discard")

	log.SetOutput(os.Stderr)
	log.Println("Hello, World") // => 2009/11/10 23:00:00 Hello, World
}

Go Playground

main.init() などで一度 log.SetOutput を実行しておけば、 別パッケージ にて log.Print している箇所全ての出力を抑止することができて便利だ。

コンソールアプリケーションなので、 flag.BoolVar との組み合わせで 以下の様な実装を行えば、verbose mode が実現できる。

package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

var verbose bool

func init() {

	flag.BoolVar(&verbose, "verbose", false, "Print a verbose message")
	flag.Parse()

	if verbose {
		log.SetOutput(os.Stderr)
	} else {
		log.SetOutput(ioutil.Discard)
	}
}

func main() {
	// ... log.Print() を使った何か ...
}

Go Playground

こうしておけば、実行時に詳細ログだけ別ファイルにリダイレクトするなどもでき、 利用しやすくなると思う。

$ ./hello Golang
Hello, Golang.

$ ./hello -verbose Golang
2019/11/19 15:56:05 Start main proc
2019/11/19 15:56:05 Number of Args:  1
2019/11/19 15:56:05 name:  Golang
Hello, Golang.
2019/11/19 15:56:05 End main proc

$ ./hello -verbose Golang 2>./detail.log
Hello, Golang.

$ cat detail.log
2019/11/19 15:56:16 Start main proc
2019/11/19 15:56:16 Number of Args:  1
2019/11/19 15:56:16 name:  Golang
2019/11/19 15:56:16 End main proc

解説

log.SetOutput は、標準ロガーの出力先を引数で指定された io.Writer に切り替える。 log.Println は指定がなければ この.標準ロガー を使用する。

log.SetOutput.jpg

// https://golang.org/src/log/log.go 抜粋
var std = New(os.Stderr, "", LstdFlags)

// ...

// SetOutput sets the output destination for the standard logger.
func SetOutput(w io.Writer) {
	std.mu.Lock()
	defer std.mu.Unlock()
	std.out = w
}

一方、 ioutil.Discardio.Writer を実装した構造体で、 Write() 実行時に入力を捨てる用な実装がなされているため、 結果として log.Println() の内部で書き出された内容は 捨てられる様になる。

ioutil.Discard.jpg

// https://golang.org/src/io/ioutil/ioutil.go 抜粋
type devNull int

// devNull implements ReaderFrom as an optimization so io.Copy to
// ioutil.Discard can avoid doing unnecessary work.
var _ io.ReaderFrom = devNull(0)

func (devNull) Write(p []byte) (int, error) {
	return len(p), nil
}

// Discard is an io.Writer on which all Write calls succeed
// without doing anything.
var Discard io.Writer = devNull(0)

参考


comments powered by Disqus