Writers パッケージを作ってみた

Twitter で

というのを見かけたので,試しに作ってみた。

いや tee および grep コマンドを組み合わせれば出力の分割はできるんだけどね1。 まぁ,言語的に面白いトピックはないし,手遊びということで。

たとえば,拙作の logf パッケージを使ってこんなログ出力を考えてみる。

package main

import (
    "os"

    "github.com/spiegel-im-spiegel/logf"
)

func main() {
    logf.SetOutput(os.Stdout)
    for i := 0; i < 6; i++ {
        logf.SetMinLevel(logf.TRACE + logf.Level(i))
        logf.Tracef("Traceing: No. %d\n", i+1)
        logf.Debugf("Debugging: No. %d\n", i+1)
        logf.Printf("Information: No. %d\n", i+1)
        logf.Warnf("Warning: No. %d\n", i+1)
        logf.Errorf("Erroring: No. %d\n", i+1)
        logf.Fatalf("Fatal Erroring: No. %d\n", i+1)
    }
}

これを実行すると,こんな感じになる。

$ go run sample.go
2020/03/28 14:44:44 [TRACE] Traceing: No. 1
2020/03/28 14:44:44 [DEBUG] Debugging: No. 1
2020/03/28 14:44:44 [INFO] Information: No. 1
2020/03/28 14:44:44 [WARN] Warning: No. 1
2020/03/28 14:44:44 [ERROR] Erroring: No. 1
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 1
2020/03/28 14:44:44 [DEBUG] Debugging: No. 2
2020/03/28 14:44:44 [INFO] Information: No. 2
2020/03/28 14:44:44 [WARN] Warning: No. 2
2020/03/28 14:44:44 [ERROR] Erroring: No. 2
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 2
2020/03/28 14:44:44 [INFO] Information: No. 3
2020/03/28 14:44:44 [WARN] Warning: No. 3
2020/03/28 14:44:44 [ERROR] Erroring: No. 3
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 3
2020/03/28 14:44:44 [WARN] Warning: No. 4
2020/03/28 14:44:44 [ERROR] Erroring: No. 4
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 4
2020/03/28 14:44:44 [ERROR] Erroring: No. 5
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 5
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 6

これを出発点とする。

出力を多重化するには io.MultiWriter() 関数を使うとよい。 こんな感じ。

package main

import (
    "fmt"
    "io"
    "os"

    "github.com/spiegel-im-spiegel/logf"
)

func main() {
    file, err := os.Create("log.txt")
    if err != nil {
        fmt.Printf("%#v\n", err)
        return
    }
    defer file.Close()

    ws := io.MultiWriter(
        file,
        os.Stdout,
    )
    logf.SetOutput(ws)
    for i := 0; i < 6; i++ {
        logf.SetMinLevel(logf.TRACE + logf.Level(i))
        logf.Tracef("Traceing: No. %d\n", i+1)
        logf.Debugf("Debugging: No. %d\n", i+1)
        logf.Printf("Information: No. %d\n", i+1)
        logf.Warnf("Warning: No. %d\n", i+1)
        logf.Errorf("Erroring: No. %d\n", i+1)
        logf.Fatalf("Fatal Erroring: No. %d\n", i+1)
    }
}

これで標準出力と log.txt ファイルに全く同じ内容が出力される。

次に,標準出力には [ERROR][FATAL] のログのみ出力したい。 そこでこんな型を考える。

package writers

//FilterWriter type is Writer with filter
type FilterWriter struct {
    word   []byte
    writer io.Writer
}

この型に対して以下の Write() メソッド

//Write function writes bytes data.
func (w *FilterWriter) Write(b []byte) (int, error) {
    if w.match(b) {
        return w.writer.Write(b)
    }
    return len(b), nil
}

func (w *FilterWriter) match(b []byte) bool {
    if len(b) == 0 {
        return false
    }
    if w.word == nil {
        return true
    }
    return bytes.Contains(b, w.word)
}

を組み込めば,設定したキーワードを含んでいる場合のみ書き込みを行うようになる。

writers.FilterWriter を使って先程のコードを書き換えてみよう。 こんな感じ。

package main

import (
    "fmt"
    "io"
    "os"

    "github.com/spiegel-im-spiegel/logf"
    "github.com/spiegel-im-spiegel/writers"
)

func main() {
    file, err := os.Create("log.txt")
    if err != nil {
        fmt.Printf("%#v\n", err)
        return
    }
    defer file.Close()

    ws := io.MultiWriter(
        file,
        writers.Filter(os.Stdout, []byte("[ERROR]")),
		writers.Filter(os.Stdout, []byte("[FATAL]")),
    )
    logf.SetOutput(ws)
    for i := 0; i < 6; i++ {
        logf.SetMinLevel(logf.TRACE + logf.Level(i))
        logf.Tracef("Traceing: No. %d\n", i+1)
        logf.Debugf("Debugging: No. %d\n", i+1)
        logf.Printf("Information: No. %d\n", i+1)
        logf.Warnf("Warning: No. %d\n", i+1)
        logf.Errorf("Erroring: No. %d\n", i+1)
        logf.Fatalf("Fatal Erroring: No. %d\n", i+1)
    }
}

これで標準出力が

$ go run sample.go
2020/03/28 14:44:44 [ERROR] Erroring: No. 1
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 1
2020/03/28 14:44:44 [ERROR] Erroring: No. 2
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 2
2020/03/28 14:44:44 [ERROR] Erroring: No. 3
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 3
2020/03/28 14:44:44 [ERROR] Erroring: No. 4
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 4
2020/03/28 14:44:44 [ERROR] Erroring: No. 5
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 5
2020/03/28 14:44:44 [FATAL] Fatal Erroring: No. 6

となった。

単純な比較のみだと複雑なパターンを構成し辛いので,正規表現バージョンも作ってみた。

//RegexpWriter type is Writer with regular expression filter
type RegexpWriter struct {
    re     *regexp.Regexp
    writer io.Writer
}

//WriteString function writes string.
func (w *RegexpWriter) Write(b []byte) (int, error) {
    if w.match(b) {
        return w.writer.Write(b)
    }
    return len(b), nil
}

func (w *RegexpWriter) match(b []byte) bool {
    if len(b) == 0 {
        return false
    }
    if w.re == nil {
        return true
    }
    return w.re.Match(b)
}

これを使えば,先程のコードはこんな感じにできる。

package main

import (
    "fmt"
    "io"
    "os"
    "regexp"

    "github.com/spiegel-im-spiegel/logf"
    "github.com/spiegel-im-spiegel/writers"
)

func main() {
    file, err := os.Create("log.txt")
    if err != nil {
        fmt.Printf("%#v\n", err)
        return
    }
    defer file.Close()

    ws := io.MultiWriter(
        file,
        writers.FilterRegexp(os.Stdout, regexp.MustCompile(`\[(ERROR|FATAL)\]`)),
    )
    logf.SetOutput(ws)
    for i := 0; i < 6; i++ {
        logf.SetMinLevel(logf.TRACE + logf.Level(i))
        logf.Tracef("Traceing: No. %d\n", i+1)
        logf.Debugf("Debugging: No. %d\n", i+1)
        logf.Printf("Information: No. %d\n", i+1)
        logf.Warnf("Warning: No. %d\n", i+1)
        logf.Errorf("Erroring: No. %d\n", i+1)
        logf.Fatalf("Fatal Erroring: No. %d\n", i+1)
    }
}

これで同じ結果が得られる。

今回はベースの Writer に出力多重化やらフィルタやらの機能を被せているだけなので,色々と応用が効くだろう。 効くといいな(笑)

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. ちなみに Go の標準パッケージにも io.TeeReader() 関数ってのがあって tee コマンドと同等のことができる。 ↩︎