こんにちは。KOUKIです。Go言語でWeb開発をしてます。
本記事では、業務でよく実装するファイル操作をgoroutineを使った並行処理で実装しています。※記事の前半はgoroutineを使用せず、逐次処理で実装しています。goroutineを使うのは記事の最後になります。
5~10分くらいで読み終わると思いますが、時間がないよって人は、コードサンプルを確認してください。
ファイル操作については、以下の記事も参考になると思います。
<目次>
ワークスペースの作成
ワークスペースを準備しましょう。
1 2 3 4 5 6 7 8 9 |
mkdir fileHandler cd fileHandler touch main.go touch empTxt.txt touch empTxt2.txt touch empTxt3.txt touch text1.txt | echo Hello World >> text.txt touch text2.txt | echo Hello World >> text.txt touch text3.txt | echo Hello World >> text.txt |
1 2 3 4 5 6 7 |
// main.go package main func main() { } |
実装
これから作成するプログラムは、terminal上で動きます。
引数から渡されたディレクトリ名からデータを読み取り、ファイルサイズが0のファイル名をtextに出力する簡単なプログラムです。
では、早速実装しましょう。
引数の読み込み
最初に、ターミナルから渡したディレクトリをプログラム側で受け取ります。その為には、osパッケージのArgsメソッドを使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package main import ( "fmt" "os" ) func receivedArgs() []string { args := os.Args[1:] if len(args) == 0 { fmt.Println("ディレクトリパスを指定して下さい。") return nil } return args } func main() { args := receivedArgs() if args == nil { return } } |
1 2 3 4 5 6 7 8 |
$ go run main.go . Args: []string{"."} $ go run main.go filehandler Args: []string{"filehandler"} $ go run main.go ディレクトリパスを指定して下さい。 |
ディレクトリの読み込み
次は、ディレクトリパスの情報を読み込む関数を作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
func main() { args := receivedArgs() if args == nil { return } files, err := ioutil.ReadDir(args[0]) if err != nil { fmt.Println(err) return } } |
ioutil.ReadDirメソッドにて、ディレクトリパスの情報から格納されているファイルの情報を読み取ることができます。
ファイル情報の出力
読み取ったファイルの情報をTerminal上に表示させてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func printFileInfo(files []os.FileInfo) { for _, file := range files { // ファイルサイズ0の情報だけだす if file.Size() == 0 { name := file.Name() fmt.Println(name) } } } func main() { args := receivedArgs() if args == nil { return } files, err := ioutil.ReadDir(args[0]) if err != nil { fmt.Println(err) return } printFileInfo(files) } |
ファイルサイズが0のものだけ、ターミナル上に出力されるプログラムを書きました。
早速、実行してみましょう。
1 2 3 4 5 6 7 |
$ go run main.go . empTxt.txt empTxt2.txt empTxt3.txt text1.txt text2.txt text3.txt |
問題なく表示されましたね。
ioutil.ReadDirメソッドで読み取ったデータは、FileInfoインターフェースを持ちます。
1 2 3 4 5 6 7 8 |
type FileInfo interface { Name() string // base name of the file Size() int64 // length in bytes for regular files; system-dependent for others Mode() FileMode // file mode bits ModTime() time.Time // modification time IsDir() bool // abbreviation for Mode().IsDir() Sys() interface{} // underlying data source (can return nil) } |
これを使って、ファイルの情報にアクセスしています。
ファイル情報の書き込み
Size 0のファイルの情報を出力できるようになったので、新しいファイル(out.txt)にその情報を書き込む処理を実装してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func writeFileInfo(files []os.FileInfo, names []byte) { for _, file := range files { if file.Size() == 0 { name := file.Name() // string(name)は、連続したbyteなので、...で分割すれば[]byte typeにも格納可能 names = append(names, name...) names = append(names, '\n') } } // ファイルの書き込み err := ioutil.WriteFile("out.txt", names, 0644) if err != nil { fmt.Println(err) return } fmt.Printf("%s\n", names) } func main() { args := receivedArgs() if args == nil { return } // ファイル名格納用の変数 var names []byte files, err := ioutil.ReadDir(args[0]) if err != nil { fmt.Println(err) return } // 空のファイル名をout.txtに書き出す writeFileInfo(files, names) } |
上記では、書き込み用関数のwriteFileInfoを実装しました。
書き込みは、ioutil.WriteFileにて行なっています。
1 |
func WriteFile(filename string, data []byte, perm os.FileMode) error |
情報を書き込むためには、[]byte型のデータを渡す必要があります。
そこで、ワンポイントと呼べる処理が一つあります。
1 |
names = append(names, name...) |
namesは[]byte型ですが、nameはstring型です。本来であれば型が違う為、appendできませんが、省略記号「…」を付与するとstringが分割されたbyteとして格納されるようです。stringは連続したbyte型なんですね。
このプログラムを実行してみましょう。
1 2 3 4 5 6 |
$ go run main.go . empTxt.txt empTxt2.txt empTxt3.txt text1.txt text2.txt |
実行が完了するとout.txtが作成されているはずです。
1 2 3 4 5 6 7 |
// out.txt empTxt.txt empTxt2.txt empTxt3.txt text1.txt text2.txt text3.txt |
キャパシティを設定する
out.txtの容量を確認してみましょう。
1 2 |
$ ls -l out.txt -rw-r--r-- 1 XXX staff 65 12 9 07:00 out.txt |
だいたい65byteです。先ほどまでのプログラムでは、「var names []byte」のようにキャパシティを設定せずにbyteを追加してましたが、プログラミング的には非効率です。
ここもワンポイントなのですが、「キャパシティを設定する方が処理速度が早くなる」ことがあります。
キャパシティを設定するには、make関数が使えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
func getCapSize(files []os.FileInfo) int { var total int for _, file := range files { if file.Size() == 0 { // +1 => \nの分 total += len(file.Name()) + 1 } } return total } func main() { args := receivedArgs() if args == nil { return } files, err := ioutil.ReadDir(args[0]) // キャパシィティを決めた方が処理速度が早くなる total := getCapSize(files) fmt.Printf("トータル: %d bytes.\n", total) names := make([]byte, 0, total) if err != nil { fmt.Println(err) return } // 空のファイル名をout.txtに書き出す writeFileInfo(files, names) } |
キャパシティを測るgetCapSize関数を実装しました。ここで対象のファイル名と「\n」分(+1)のbyte数を計算し、呼び出し元に返します。
そしてこの数値を「names := make([]byte, 0, total)」とすることで、キャパシティを指定することができます。
プログラムを実行してみましょう。
1 2 3 4 5 6 7 8 |
$ go run main.go . トータル: 65 bytes. empTxt.txt empTxt2.txt empTxt3.txt text1.txt text2.txt text3.txt |
「トータル: 65 bytes.」と出力されているので、out.txtと同じ容量のキャパシティが取得できたことがわかります。
並行処理
これまで実装したプログラムをgoroutineを使って、並列化できないか考えてみましょう。
このプログラムでは、引数に渡したディレクトリ単位で処理を走らせることができます。そのため、ディレクトリ検索から並行化できそうです。
検証のため、ディレクトリとファイルを作成しましょう。
1 2 3 4 5 6 7 8 9 |
# 新しいディレクトリとファイルを作成する。 mkdir PUB mkdir SUB touch PUB/pub_emptyTxt1.txt touch PUB/pub_emptyTxt2.txt touch PUB/pub_emptyTxt3.txt touch SUB/sub_emptyTxt1.txt touch SUB/sub_emptyTxt2.txt touch SUB/sub_emptyTxt3.txt |
まず、引数ごとにファイルを処理できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// dirNameを渡す func writeFileInfo(files []os.FileInfo, names []byte, dirName string) { for _, file := range files { if file.Size() == 0 { name := file.Name() // string(name)は、連続したbyteなので、...で分割すれば[]byte typeにも格納可能 names = append(names, name...) names = append(names, '\n') } } // ファイルの書き込み err := ioutil.WriteFile(dirName+"_out.txt", names, 0644) if err != nil { fmt.Println(err) return } fmt.Printf("%s\n", names) } func main() { args := receivedArgs() if args == nil { return } // 引数ごとにファイルを処理できるように変更 for _, dirName := range args { files, err := ioutil.ReadDir(dirName) total := getCapSize(files) fmt.Printf("トータル: %d bytes.\n", total) names := make([]byte, 0, total) if err != nil { fmt.Println(err) return } writeFileInfo(files, names, dirName) } } |
今までは、
のように渡した引数の最初の値のみ処理をしていましたが、この処理をfiles, err := ioutil.ReadDir(args[0])
に変更しました。for _, dirName := range args
このようにすることで、渡した引数文処理を走らせることが可能になります。
1 2 3 4 5 6 7 8 9 10 |
$ go run main.go PUB SUB トータル: 54 bytes. pub_emptyTxt1.txt pub_emptyTxt2.txt pub_emptyTxt3.txt トータル: 54 bytes. sub_emptyTxt1.txt sub_emptyTxt2.txt sub_emptyTxt3.txt |
writeFileInfo関数にディレクトリ名(dirName)を渡し、出力ファイル名としたので、以下のファイルが出力されているはずです。
1 2 |
* PUB_out.txt * SUB_out.txt |
ファイルの中身を確認すると問題なくファイル名が出力されていることがわかります。
1 2 3 4 5 6 7 8 9 10 |
* PUB_out.txt pub_emptyTxt1.txt pub_emptyTxt2.txt pub_emptyTxt3.txt * SUB_out.txt sub_emptyTxt1.txt sub_emptyTxt2.txt sub_emptyTxt3.txt |
ここまでの処理を並行化します。
並行処理のチップスを次の記事に記載したので、よかったら参考にしてください。
では、早速実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func main() { args := receivedArgs() if args == nil { return } // WaitGroupを作成 var wg sync.WaitGroup semaphore := make(chan struct{}, 10) for _, dirName := range args { wg.Add(1) semaphore <- struct{}{} go func(dirName string) { // clean up defer func() { defer wg.Done() <-semaphore }() // ReadDirの読み込みが遅くなるかもしれないので、ここから並列化する files, err := ioutil.ReadDir(dirName) if err != nil { fmt.Println(err) return } total := getCapSize(files) fmt.Printf("トータル: %d bytes.\n", total) names := make([]byte, 0, total) writeFileInfo(files, names, dirName) }(dirName) } wg.Wait() } |
プログラムを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 |
$ go run main.go PUB SUB トータル: 54 bytes. トータル: 54 bytes. sub_emptyTxt1.txt sub_emptyTxt2.txt sub_emptyTxt3.txt pub_emptyTxt1.txt pub_emptyTxt2.txt pub_emptyTxt3.txt |
処理は問題なく完了し、PUB_/SUB_out.txtも出力できました。
今回のプログラムは規模が小さいのであまり関係ありませんが、ディレクトリの格納ファイルの増加に伴い
の読み込み部分がコストになると思ったので、goroutineでこの処理から並行化しています。files, err := ioutil.ReadDir(dirName)
おわりに
goroutineの使い所として、本記事のように処理の順番によって結果が変わらないようなプログラムには有効だと思います。
goroutineは本当に便利なので、使いこなしていきたいですね^^
コードサンプル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
package main import ( "fmt" "io/ioutil" "os" "sync" ) func receivedArgs() []string { args := os.Args[1:] if len(args) == 0 { fmt.Println("ディレクトリパスを指定して下さい。") return nil } return args } func writeFileInfo(files []os.FileInfo, names []byte, dirName string) { for _, file := range files { if file.Size() == 0 { name := file.Name() names = append(names, name...) names = append(names, '\n') } } err := ioutil.WriteFile(dirName+"_out.txt", names, 0644) if err != nil { fmt.Println(err) return } fmt.Printf("%s\n", names) } func getCapSize(files []os.FileInfo) int { var total int for _, file := range files { if file.Size() == 0 { total += len(file.Name()) + 1 } } return total } func main() { args := receivedArgs() if args == nil { return } var wg sync.WaitGroup semaphore := make(chan struct{}, 10) for _, dirName := range args { wg.Add(1) semaphore <- struct{}{} go func(dirName string) { defer func() { defer wg.Done() <-semaphore }() files, err := ioutil.ReadDir(dirName) if err != nil { fmt.Println(err) return } total := getCapSize(files) fmt.Printf("トータル: %d bytes.\n", total) names := make([]byte, 0, total) writeFileInfo(files, names, dirName) }(dirName) } wg.Wait() } |
最近のコメント