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 用に置き換えることができるのだ。
こうやって混沌としたコードを整理していくんですねぇ。
参考図書
- プログラミング言語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 言語の教科書と言ってもいいだろう。
- 初めてのGo言語 ―他言語プログラマーのためのイディオマティックGo実践ガイド
- Jon Bodner (著), 武舎 広幸 (翻訳)
- オライリージャパン 2022-09-26
- 単行本(ソフトカバー)
- 4814400047 (ASIN), 9784814400041 (EAN), 4814400047 (ISBN)
- 評価
2021年に出た “Learning Go” の邦訳版。私は版元で PDF 版を購入。 Go 特有の語法(idiom)を切り口として Go の機能やパッケージを解説している。 Go 1.19 対応。
- 実用 Go言語 ―システム開発の現場で知っておきたいアドバイス
- 渋川 よしき (著), 辻 大志郎 (著), 真野 隼記 (著)
- オライリージャパン 2022-04-22
- 単行本(ソフトカバー)
- 4873119693 (ASIN), 9784873119694 (EAN), 4873119693 (ISBN)
- 評価
版元のデジタル版を購入。 Go で躓きやすい点を解説していくのが最初の動機らしい。「◯◯するには」を調べる際にこの本を調べるといいかも。
- Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)
- Robert C.Martin (著), 角 征典 (著), 高木 正弘 (著)
- ドワンゴ 2018-08-01 (Release 2018-08-01)
- Kindle版
- B07FSBHS2V (ASIN)
- 評価
実務に即効性のある技術解説書というわけではないが,ものの「考え方」を示す本としてはよく出来ている。ソフトウェア技術史の読み物としても面白い。
-
「メソッド値」については拙文「#golang メソッド式とメソッド値」を参考にどうぞ。 ↩︎
-
CSV データを扱う便利パッケージとしては
github.com/gocarina/gocsv
が有名なのだが,あれって Unmarshal 時に CSV データ全体を構造体の配列にしてしまうのが気に食わないんだよなぁ。 CSV データが百万レコードあったら百万個の配列を作ってしまう。まぁ,巨大データを扱う前提ではないということなんだろうけど。 ↩︎