BOM を除去する io.ReadCloser Decorator を作ってみた

先日 Zenn で UTF-8 BOM (Byte Order Mark) を除去する Decorator を紹介したのだが

最後の

package main

import (
    "encoding/csv"
    "errors"
    "fmt"
    "io"
    "os"

    "github.com/spkg/bom"
)

func main() {
    file, err := os.Open("./sample3.csv")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    defer file.Close()

    r := csv.NewReader(bom.NewReader(file))
    for {
        row, err := r.Read()
        if err != nil {
            if !errors.Is(err, io.EOF) {
                fmt.Fprintln(os.Stderr, err)
                return
            }
            break
        }
        fmt.Println(row)
    }
}

os.File 型, bom.NewReader() 関数が返す io.Reader 抽象型とその実体である bufio.Reader,そして csv.Reader 型の3つを意識する必要がある。 もう少し詳しく言うと Close() メソッドを持っているのはベースの os.File 型のみなのでファイルを閉じるためには os.File 型の変数を保持っておく必要があるのだ。 これは上手いやり方ではないなぁ,というのが記事を書いた後の感想だった。

そこで github.com/spkg/bom パッケージを参考に,自前で github.com/goark/utf8bom パッケージを作ってみた。 このパッケージの Reader 型は

type Reader struct {
    *bufio.Reader
    closer func() error
}

という構成になっていて,埋め込みの bufio.Reader フィールド以外に closer を持っている。 初期化時に

func Strip(r io.Reader) *Reader {
    closer := func() error { return nil }
    if c, ok := r.(io.Closer); ok {
        closer = c.Close
    }
    br := &Reader{Reader: bufio.NewReader(r), closer: closer}
    b, err := br.Peek(3)
    if err != nil {
        return br
    }
    if bytes.Equal(b, []byte{0xef, 0xbb, 0xbf}) { // compare BOM
        _, _ = br.Discard(3)
    }
    return br
}

てな感じに closer フィールドにメソッド値1 をセットすることで utf8bom.Reader.Close() メソッド

func (r *Reader) Close() error {
    return r.closer()
}

起動時にベースの Close() メソッドへ処理を委譲できるようにした。 こういうときに変数の生存期間とか考えなくていい Go は便利だよねぇ。

これを使って最初のサンプルコードを少し書き換えてみる。

package main

import (
    "encoding/csv"
    "errors"
    "fmt"
    "io"
    "os"

    "github.com/goark/utf8bom"
)

func openCsvFile(path string) (io.ReadCloser, error) {
    file, err := os.Open("./sample3.csv")
    if err != nil {
        return nil, err
    }
    return utf8bom.Strip(file), nil
}

func main() {
    file, err := openCsvFile("./sample3.csv")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    defer file.Close()

    r := csv.NewReader(file)
    for {
        row, err := r.Read()
        if err != nil {
            if !errors.Is(err, io.EOF) {
                fmt.Fprintln(os.Stderr, err)
                return
            }
            break
        }
        fmt.Println(row)
    }
}

これで CSV ファイル・オープン時の仔細を掃き出して io.ReadCloser 型で扱えるようになった。 ここまで来れば csv.NewReader() 関数もどうにかして openCsvFile() 関数に掃き出したいよね。

ちうわけで,拙作の github.com/goark/csvdata パッケージも導入する。 こんな感じ。

package main

import (
    "errors"
    "fmt"
    "io"
    "os"

    "github.com/goark/csvdata"
    "github.com/goark/utf8bom"
)

func openCsvFile(path string) (*csvdata.Rows, error) {
    file, err := os.Open("./sample3.csv")
    if err != nil {
        return nil, err
    }
    return csvdata.NewRows(csvdata.New(utf8bom.Strip(file)), true), nil
}

func main() {
    file, err := openCsvFile("./sample3.csv")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    defer file.Close()

    for {
        if err := file.Next(); err != nil {
            if !errors.Is(err, io.EOF) {
                fmt.Fprintln(os.Stderr, err)
                return
            }
            break
        }
        fmt.Println(file.Row())
    }
}

これでメイン側ではデータの行・列構造のみ注視すればよくなった2

ちなみに github.com/goark/csvdata パッケージは Excel ファイルにも対応していて,同じ csvdata.Rows 型に落とし込んで扱えるようになっている。 つまり上のサンプルコードの openCsvFile() 関数の中身をまるっと Excel 用に置き換えることができるのだ。

こうやって混沌としたコードを整理していくんですねぇ。

参考図書

photo
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
丸善出版 2016-06-20
単行本(ソフトカバー)
4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
評価     

著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。

reviewed by Spiegel on 2016-07-13 (powered by PA-APIv5)

photo
初めてのGo言語 ―他言語プログラマーのためのイディオマティックGo実践ガイド
Jon Bodner (著), 武舎 広幸 (翻訳)
オライリージャパン 2022-09-26
単行本(ソフトカバー)
4814400047 (ASIN), 9784814400041 (EAN), 4814400047 (ISBN)
評価     

2021年に出た “Learning Go” の邦訳版。私は版元で PDF 版を購入。 Go 特有の語法(idiom)を切り口として Go の機能やパッケージを解説している。 Go 1.19 対応。

reviewed by Spiegel on 2022-10-11 (powered by PA-APIv5)

photo
実用 Go言語 ―システム開発の現場で知っておきたいアドバイス
渋川 よしき (著), 辻 大志郎 (著), 真野 隼記 (著)
オライリージャパン 2022-04-22
単行本(ソフトカバー)
4873119693 (ASIN), 9784873119694 (EAN), 4873119693 (ISBN)
評価     

版元のデジタル版を購入。 Go で躓きやすい点を解説していくのが最初の動機らしい。「◯◯するには」を調べる際にこの本を調べるといいかも。

reviewed by Spiegel on 2022-10-26 (powered by PA-APIv5)

photo
Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)
Robert C.Martin (著), 角 征典 (著), 高木 正弘 (著)
ドワンゴ 2018-08-01 (Release 2018-08-01)
Kindle版
B07FSBHS2V (ASIN)
評価     

実務に即効性のある技術解説書というわけではないが,ものの「考え方」を示す本としてはよく出来ている。ソフトウェア技術史の読み物としても面白い。

reviewed by Spiegel on 2021-04-03 (powered by PA-APIv5)


  1. 「メソッド値」については拙文「#golang メソッド式とメソッド値」を参考にどうぞ。 ↩︎

  2. CSV データを扱う便利パッケージとしては github.com/gocarina/gocsv が有名なのだが,あれって Unmarshal 時に CSV データ全体を構造体の配列にしてしまうのが気に食わないんだよなぁ。 CSV データが百万レコードあったら百万個の配列を作ってしまう。まぁ,巨大データを扱う前提ではないということなんだろうけど。 ↩︎