64
45

More than 5 years have passed since last update.

Go言語らしいLoggingについて

Last updated at Posted at 2018-07-14

Go言語らしいLogging

Go言語でlogを扱う場合、サードパーティのロギングライブラリを使う人は少なくないと思います。
理由としては標準のlogパッケージの機能の貧弱さ、特にレベルが無いというのが多いと思います。
しかしGo言語原理主義的にはやっぱり標準を使いたいですよね。
安心してください、標準logパッケージにはレベルがありませんがレベルについて考慮していないというわけではありません。
ロギングパッケージとしての役割が他の言語でも見られる通常のログライブラリと異なるだけです。

ログレベルについてですが
そもそもログレベルは基本的に統一されていません、DebugLevelがなかったりWarningLevelがなかったり文字列で表現したり色々あると思います。
統一されていないのが問題なのでなく、プログラムの対象によって必要なログの機能というのは異なるのは当然なので
本来必要なレベルは自分で宣言できるべきです。
例えばUnixコマンドであれば標準出力と標準エラー出力があれば十分なケースが多いです。
反対に大それたものになってくると警告レベルだって欲しくなるはずです。
文字列は文字列で出力先の切り替えとかになってくるとちょっとややこしいですよね。
かといってパッケージレベルであらゆるプロジェクトに対応できるように機能を追加すればライブラリは肥大化し、柔軟性とシンプルさが失われます。

Go言語のlogパッケージは低機能ですが最低限の実装を肩代わりしてくれます。
小さなものであれば単体でも使えますが多少凝ったものが欲しくなれば外部ライブラリを使うのでなく
ある程度ラップして使うことでGo言語らしいプログラムにつながるかもしれません。

例としてはスケールの小さいものは公式のGo Playgroundのソース、
大きいものはロブ・パイク先生も開発してるgoogleプロジェクトのupspinのソースが良いと思います。

Go Playground

まずGo Playgroundの実装を見てみましょう。
サブパッケージの存在しないスケールの小さなサーバープログラムです。
logger.goというログパッケージをラップするファイルがあります

type logger interface {
    Printf(format string, args ...interface{})
    Errorf(format string, args ...interface{})
    Fatalf(format string, args ...interface{})
}

// stdLogger implements the logger interface using the log package.
// There is no need to specify a date/time prefix since stdout and stderr
// are logged in StackDriver with those values already present.
type stdLogger struct {
    stderr *stdlog.Logger
    stdout *stdlog.Logger
}

func newStdLogger() *stdLogger {
    return &stdLogger{
        stdout: stdlog.New(os.Stdout, "", 0),
        stderr: stdlog.New(os.Stderr, "", 0),
    }
}

func (l *stdLogger) Printf(format string, args ...interface{}) {
    l.stdout.Printf(format, args...)
}

func (l *stdLogger) Errorf(format string, args ...interface{}) {
    l.stderr.Printf(format, args...)
}

func (l *stdLogger) Fatalf(format string, args ...interface{}) {
    l.stderr.Fatalf(format, args...)
}

まずloggerインターフェースが定義されています。
Go言語のInterfaceでよく見かける使い方で実装側が実装を隠すのではなく要求側が実装を気にしないために必要なものを定義します。
内容はPrintfは標準出力に、Errorfは標準エラー出力、Fatalfは標準エラー出力+終了という形で使うものだけ宣言しています。
そして内部で標準logパッケージを扱いフォーマット関係は任せていて、やっているのはただのラップです。
一々ロギングライブラリを実装しなければいけないのかという気持ちにならないでください。
これは宣言のようなものです。このプログラムにとって最適なロギングのAPIを宣言しているだけです。
でも一々ロギングライブラリを実装しなければいけないのはめんどくさいので外部ライブラリ使いましょう。

upspin

次の例ですが本格的なプロジェクトでサブパッケージ盛りだくさんのupspinです。
今回、サブパッケージとしてlogライブラリが存在します。

少し話はずれますが、
Go言語で標準ライブラリのラッパーを作ることは少なくありません。
特にerrorsやlogあたりは多いです。そういった場合、オリジナリティあふれるイラマチオボンバーみたいな名前をつけたり、名前をかぶらせまいと健気にerrgoみたいな名前をつけたりする人がいるかもしれませんが
同じことをしようとしているのでそのまま名前をかぶせましょう。役割で名前をつけましょう。必要なのはlogdragonだとかerrgoblinだとかでなくエラーを扱うパッケージとロギングを扱うパッケージです。つまりerrorsとlogです。やりたいことが明示的ですいいですね。

話は戻ってupspinのlogパッケージですが長いので途中まで載せます。

// Logger is the interface for logging messages.
type Logger interface {
    // Printf writes a formated message to the log.
    Printf(format string, v ...interface{})

    // Print writes a message to the log.
    Print(v ...interface{})

    // Println writes a line to the log.
    Println(v ...interface{})

    // Fatal writes a message to the log and aborts.
    Fatal(v ...interface{})

    // Fatalf writes a formated message to the log and aborts.
    Fatalf(format string, v ...interface{})
}

// Level represents the level of logging.
type Level int

// Different levels of logging.
const (
    DebugLevel Level = iota
    InfoLevel
    ErrorLevel
    DisabledLevel
)

// ExternalLogger describes a service that processes logs.
type ExternalLogger interface {
    Log(Level, string)
    Flush()
}

// The set of default loggers for each log level.
var (
    Debug = &logger{DebugLevel}
    Info  = &logger{InfoLevel}
    Error = &logger{ErrorLevel}
)

見てください今回はInterfaceからError系がなくなりPrint系とFatal系だけになっています。
見てくださいロガーインスタンスをレベルごとに持ちabortの有無とレベルを分けています。
しかも見てくださいPrint系は無印、+ln、+f全部ありますがFatal系は+lnがありません、本当に必要なものしか定義してませんね。潔いですね。はい外部ライブラリ使いましょう。

以上

細かい工夫とか実装は例のソースの方を見てください。
upspinの方は特にログ以外でも色々参考になるのでおすすめです。

とにかくGo言語ではlogの実装との間にクッションを置く形であれば標準ライブラリの貧弱さは特別問題ではありませんむしろ強い。外部ライブラリに合わせるのでなく俺が使うんだからお前が合わせろという気持ちで臨みましょう。

必要なものを定義したInterfaceを用意することでライブラリや実装の事情だとかを無視できます。
外部ライブラリを使わない場合は依存関係の削減もできますし使ってもクッションがあれば差し替えも容易です。例えばテストの際に、*testing.Tを内部で持ち出力系の関数をloggerのinterfaceにマッピングしたもので差し替えるなどできます。
熱いですね。
また、ログレベル以外に機能がほしい場合、それはlogの機能でなく他のなにかの役割でないかなどを考慮してみてください。logコンストラクタにわたすWriterのラッパーを作成することで解決出来るケースもあります。

もちろん大それたフレームワークを使っていたりロギングにそこまでする必要のない場合、あと実行速度など外部ライブラリを使ったほうが良い場面はありますがGo言語のロギングは珍しい形なのでこういった選択肢もあるということを知っていただければ幸いです。
外部ライブラリ使いましょう。

64
45
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
64
45