search
LoginSignup
8

More than 1 year has passed since last update.

posted at

updated at

io.MultiWriter を使って標準出力と同じ内容のログを生成する

Go を使って職場の改善のためのコマンドラインツールを多数書いています。
シンプルなツールの場合、まずは標準出力に色々出力していきますが、そのうちログも欲しいと言われるケースが多いです。
そんな時に Go の場合はどうしているか、というと io.MultiWriter を使います。

func MultiWriter(writers ...Writer) Writer

MultiWriter creates a writer that duplicates its writes to all the provided writers,
similar to the Unix tee(1) command.

Each write is written to each listed writer, one at a time. If a listed writer returns an error,
that overall write operation stops and returns the error; it does not continue down the list.

ベースのコード

以下のようなコードがあったとします。
run() という関数を呼ぶと対応した処理が行われ、内部で標準出力に何かを出力しています。

package main

import (
    "fmt"
)

func main() {
    run()
}

func run() error {
    fmt.Printf("hello world\n")
    return nil
}

これを拡張可能とするため、そしてテストしやすくするために、まずは io.Writer() を用いて標準出力への依存を切ります。

標準出力への依存を切った Version

run() 関数の引数で io.Writer を取るように変更し、 run() 関数内では fmt.Printf() の代わりに fmt.Fprintf() を使うようにします。
これで、標準出力に直接何かを書き込む、というのはなくなりました。

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    run(os.Stdout)
}

func run(w io.Writer) error {
    fmt.Fprintf(w, "hello world\n")
    return nil
}

io.MultiWriter を使って標準出力とファイルの両方に出力するようにした Version

続いて、 os.Stdout の代わりに os.Stdout と何らかのログファイルに書き込むような io.Writer に変更します。

大分ソースコードが増えてきましたが、大事なのは io.MultiWriter() を使っている部分です。
io.MultiWriter() は引数で与えられたすべての io.Writer に対しての Write を行うような io.Writer を返します。
以下の例の場合は、 os.Stdoutfile (log.txt) の両方に書き込むようになります。

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    file, err := os.Create("log.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // ここで os.Stdout と file から新しい io.Writer を生成する
    w := io.MultiWriter(os.Stdout, file)

    run(w)
}

func run(w io.Writer) error {
    fmt.Fprintf(w, "hello world\n")
    return nil
}

引数が指定された時のみファイルにも書き込む

普段はログファイル不要だけど、時々欲しい、というようなケースは以下のように対応できます。
以下の場合は実行時に -o output.txt のように引数を足せば output.txt にも書き込むようになります。

package main

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

func main() {
    outfile := flag.String("o", "", "output")
    flag.Parse()

    var w io.Writer
    w = os.Stdout

    if *outfile != "" {
        file, err := os.Create(*outfile)
        if err != nil {
            log.Fatal(err)
        }
        defer file.Close()

        // 引数 -o が指定された時のみ、 io.Writer である w に file を足しこむ
        w = io.MultiWriter(w, file)
    }

    run(w)
}

func run(w io.Writer) error {
    fmt.Fprintf(w, "hello world\n")
    return nil
}

ログファイル側の文字コードを変更する

Go は標準だと UTF-8 のファイルを生成しますが、 Windows で開発していると (まだ Windows 7 が生きているのもあり) UTF-8 だと文字化けする、と言われるケースがあります。
本当かどうかすらも知らないですが、この場合は transform.NewWriter() を使って出力を変換すると良いです。

package main

import (
    "flag"
    "fmt"
    "io"
    "log"
    "os"

    "golang.org/x/text/encoding/japanese"
    "golang.org/x/text/transform"
)

func main() {
    outfile := flag.String("o", "", "output")
    flag.Parse()

    var w io.Writer
    w = os.Stdout

    if *outfile != "" {
        file, err := os.Create(*outfile)
        if err != nil {
            log.Fatal(err)
        }
        defer file.Close()

        // 引数 -o が指定された時のみ、 io.Writer である w に file を足しこむ
        w = io.MultiWriter(w, transform.NewWriter(file, japanese.ShiftJIS.NewEncoder()))
    }

    run(w)
}

func run(w io.Writer) error {
    fmt.Fprintf(w, "hello world こんにちは世界\n")
    return nil
}

まとめ

単に fmt.Printf() しているコードから段階的に io.MultiWriter() を使ったコードに変えていく事でログ出力にも簡単に対応できます。
職場では Go コードを書き始める時点でツールを使って雛形生成していますが、その中でもこの内容に近い事をしています。
今回紹介した方法自体はあまり実用的ではないですが、使いたいときにサクッと使えるように練習しておくと良いと思います。

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
What you can do with signing up
8