- 辻大志郎
- 約 3,500 文字
- 1,400 View
目次
Go Tips連載の第3弾目です。
はじめに
TIG の辻です。
筆者は普段ファイルを扱って何か簡単な処理をする場合は、シェルスクリプトで実装することが多かったのですが、実は Go で書くと簡単に、かつ Testable でスクリプトちっく 1 に書くことでできて、幸せになるんじゃないか? と最近考えています。
とある業務でファイルを扱い、ちょっとだけ複雑な繰り返しを要する処理をする必要がありました。手動で作業するのはめんどくさいし、(モチベーションも上がらないし) ロジックもちょっと面倒だったので、Go でスクリプトを書いて処理しました。
そこで今回はファイルを扱うようなスクリプトを Go で書くときに役に立ちそうな Tips 5 選を紹介します。
- ファイルの読み込みにio.Readerを用いる
- ファイルの書き込みにio.Writerを用いる
- リストファイルから1行ずつ読み込む
os/execを使う- ファイルパスの操作に
path/filepathを使う
Tips
1.ファイルの読み込みにio.Readerを用いる
ファイルを読み込む際に io.Reader を受け取って処理するようにすると Testable なスクリプトになって安心です。簡単な処理とはいえやはりテストは書きたいですよね。
例として、ファイルの中に「Copyright」という文字列が含まれるかどうか調べる処理を考えてみます。実装例として以下の hasCopyright のような実装が考えられます。ポイントは io.Reader のインターフェースを関数の引数として受け取ることです。
func hasCopyright(r io.Reader) (bool, error) { |
呼び元の実装例です。”LICENSE” ファイルは MIT のライセンスが書かれたファイルを Open しています。
func main() { |
hasCopyright 関数は io.Reader を受け取る関数でした。もちろん以下のように *os.File 構造体を受け取って処理できます。しかしこのようにすると、引数には *os.File 構造体を受け取る必要があり、テストケースごとにファイルを作成しないといけません。
// 非推奨: 具象の構造体を引数に取る関数 |
インターフェースである io.Reader を受け取る関数にすることで io.Reader を満たす任意の構造体を関数に渡すことができます。つまりファイル記述子を示す os.File だけでなく byte.Buffer や strings.Reader といった構造体を渡すことができます。文字列の場合は strings.NewReader を用いて string から io.Reader を生成でき便利です。以下のようにテストすることが可能になります。
package main |
go test |
テストが通ると、想定した動作をしていることがわかり、安心します。シェルスクリプトで同じような確認をしようと思うと、ちょっと手間です。
2.ファイルの書き込みにio.Writerを用いる
ファイルの読み込みに io.Reader を用いる場合と同じ話です。ファイルの書き込みに io.Writer を用いると Testable なスクリプトになります。簡単な例として、ファイルの末尾に 「Hello World.」という文字列を追記する関数を考えてみます。
// 末尾に Hello World. を書き込むサンプル実装 |
実際のファイルに書き込まなくても io.Writer を実装している bytes.Buffer に文字列を書き込み、比較してテストできます。実際にファイルを作成したい場合は os.Create などとすれば生成できます。
package main |
go test |
ちゃんと想定通り書き込めていることが分かります。
3.リストファイルから1行ずつ読み込む
ファイルに記載されている文字列を改行コードで区切って一行ずつ処理をする機会は多いと思います。シェルスクリプトだと以下のようにして各行を読み込んでなんやかんやする感じです。
while read line |
文字列リストの一覧を list.txt などとしてファイルを生成しておいて、ファイルから 1 行ずつ読み込みたいときの Go の Tips です。以下のような階層になっているとします。
. |
list.txt の中身は以下のようにリポジトリ名が書いてあるファイルとします。
future-architect/vuls |
bufio.NewScanner を用いて io.Reader (ファイルや標準入出力など)から文字列を読み込むことできます。改行コードを気にせず、各行を読み込むことができるのが嬉しいポイントです。
package main |
bufio.Reader を用いても bufio.NewScanner を用いたときと同様に読み込むことができますが io.EOF のハンドリングする必要がある分、幾分プリミティブな実装です。
func main() { |
4. os/exec を使う
ファイルを扱うスクリプトに限った話ではないですが Go では exec.Cmd を用いて外部コマンドを実行できます。とても便利です。exec.Command 関数を用いて Path と Args に実行したい文字列をセットします。外部コマンドの実行結果が不要であれば Run(), 必要であれば Output() を用いることができます。たいていの場合この 2 つのメソッドで充足することが多いです。
外部コマンドの実行した結果、エラーが発生すれば戻り値の error に値が格納されます。_ などとしてエラーを無視しないようにしましょう。ちゃんとエラーをチェックすれば直前のコマンドでエラーが発生していたけど、間違って次のコマンドが実行されてしまった。cd でエラーが発生していたけど、後続の rm が実行されて意図しないファイルやディレクトリが削除されてしまった。。。などということは防げます。Bash で set -ue しておくのと似たような雰囲気です。
その他の情報、例えば PID や終了ステータスといった実行結果を取得したい場合は Cmd 構造体に含まれる ProcessState フィールド(*os.ProcessState 型)にアクセスするといろいろな情報を取得できます。 2
package main |
注意しておきたいポイントとしてシェルスクリプトで * (ワイルドカード)を使ってコマンドを実行するときに Go の exec.Command の引数に * を含めても展開されません。以下のような実装はエラーが返ってきます。* はシェルスクリプトが展開する(glob でパターンマッチングする)ためです。
package main |
シェルスクリプトの * を使いたい場合は以下のように exec.Command の引数で明示的に /bin/sh -c とする必要があります。
b, err := exec.Command("/bin/sh", "-c", "ls", "*.go").Output() |
その他にも os.exec の Overview には、リダイレクトはされない、glob パターンの展開には filepath.Glob を用いることができる、などといった os.exec を扱う上での注意点が記載されています。あらためて確認してみてください。
5.ファイルパスの操作に path/filepath を使う
ファイルパスの結合に以下のように文字列で / を結合させて、あるディレクトリにファイルを生成することがあると思います。
testFilePath := tempDir + "/" + "test.txt" |
ファイルパス関連で問題の1つとして UNIX 系 OS と Windows でパスのセパレータが異なるという問題があります。UNIX 系 OS ではセパレータが / であって Windows では \ という話です。通常、この手のスクリプトを UNIX 系 OS と Windows の両方で動作させることは少ないと思うので、問題になることはあまりないと思いますが、path/filepath パッケージを用いるとマルチプラットフォームで扱うことができスマートです。パス/filepath パッケージは対象の OS で定義されているファイルパスと互換性のある方法でファイルパスを操作できるユーティリティを提供しているパッケージです。
以下はカレントディレクトリ直下に一時的なディレクトリ tempxxxx を作成して、その一時ディレクトリにファイルを生成する実装例です。ファイルパスの結合に filepath.Join を用いています。以下の実装では tempDir と test.txt を Join していますが、3 つ以上の文字列も Join できます。
package main |
まとめ
ファイル扱うようなスクリプトを Go で実装する上での Tips 5 選を紹介しました。io.Reader や io.Writer といったインターフェースを受け取ることでファイルを扱うスクリプトでも簡単にテストできます。エラーも明示的にハンドリングできていい感じです。ちょっとしたファイルを扱う処理を Go で書いてみてはいかがでしょうか。
- 1.スクリプトちっくとは main.go と main_test.go の 2 ファイルで簡潔に実装できる程度の処理、くらいのニュアンスで使っています。 ↩
- 2.ProcessState フィールドを参照するテクニックは Umeda.go 2020 Winter で渋川さんが話されていました。 ↩