💬
filepath.Walkとfilepath.WalkDir
この記事を書こうと思った動機
- 自分は今の仕事内容で全ファイルを列挙して調べるという作業が多いです
- その時、個人的にも好きで、プロジェクトでも推奨されているGo言語を利用して色々ツールを作っています
- その時、
filepath.Walk
を使っていましたが、ある時filepath.WalkDir
が追加されていることを最近知りました- どうやらGo 1.16で追加されたようです
-
filepath.WalkDir
がパフォーマンスいいらしいので色々調べてみました
filepath.Walk
と filepath.WalkDir
の違い
-
Issue に書いてありますがざっくりとだけ書きます
- 間違ってたらすみません
共通していること
- 第一引数の
root
に指定されたディレクトリから再帰的にファイルやディレクトリを列挙し、
第二引数のfn
でフィルタリングやエラーハンドリングする - シンボリックリンク未対応
filepath.Walk
- 最初からある方
-
fn
のコールバック関数型
type WalkFunc func(path string, info fs.FileInfo, err error) error
-
root
に指定されたディレクトリ内容をfile.ReadDir
ですべて列挙してからfn
のコールバックに渡していく
filepath.WalkDir
- Go 1.16で追加されたほう
-
fn
のコールバック関数型
type WalkDirFunc func(path string, d DirEntry, err error) error
- 先に
fn
のコールバックを呼び出してfilepath.SkipDir
が返ってきたら以降の処理を飛ばす処理を行う- このため
err != nil && err != filepath.SkipDir
の場合fn
がもう一度呼び出される可能性がある - ただ、
file.ReadDir
は高コストなので基本的には速くなるはずとのこと
- このため
検証コード
- gistにすればよかったと後で思いましたが普通にリポジトリにしました
かんたんな実装コード
- GOPATH内になるファイルを列挙する簡単なコードとしました
- 誰の環境でも試せるようにGOPATHとしましたが、検索するルートはお好みです
main.go
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/pkg/errors"
)
func findFilesWithWalk(root string) ([]string, error) {
findList := []string{}
err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return errors.Wrap(err, "failed filepath.Walk")
}
if info.IsDir() {
return nil
}
findList = append(findList, path)
return nil
})
return findList, err
}
func findFilesWithWalkDir(root string) ([]string, error) {
findList := []string{}
err := filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error {
if err != nil {
return errors.Wrap(err, "failed filepath.WalkDir")
}
if info.IsDir() {
return nil
}
findList = append(findList, path)
return nil
})
return findList, err
}
func main() {
SearchPath := os.ExpandEnv("${GOPATH}")
findFilesWalk, err := findFilesWithWalk(SearchPath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
findFilesWalkDir, err := findFilesWithWalkDir(SearchPath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 結果一致確認
// まずは検出数が同じか見る
fmt.Printf("findFilesWithWalk %v\n", len(findFilesWalk))
fmt.Printf("findFilesWithWalkDir %v\n", len(findFilesWalkDir))
// 次に内容一致確認
// 検出数が同じ状態で片方をmapのキーにして、その後キーをもう片方の要素で削除したとき
// 0になれば一致したと言えるはず
findMap := map[string]struct{}{}
for _, filename := range findFilesWalk {
findMap[filename] = struct{}{}
}
fmt.Printf("findMap %v\n", len(findMap))
for _, filename := range findFilesWalkDir {
delete(findMap, filename)
}
fmt.Printf("findMap %v\n", len(findMap))
}
実行結果
$ go run main.go
findFilesWithWalk 136220
findFilesWithWalkDir 136220
findMap 136220
findMap 0
- 内容は一致しました
-
filepath.Walk
とfilepath.WalkDir
の実装の違いはコールバック関数に引数くらいです
簡単なベンチマークコード
- 単純に上記で書いた
findFilesWithWalk
とfindFilesWithWalkDir
を回数分比較するだけのコードとします
main_test.go
package main
import (
"os"
"testing"
)
func Benchmark_findFilesWithWalkDir(b *testing.B) {
SearchPath := os.ExpandEnv("${GOPATH}")
b.ResetTimer()
for i := 0; i < b.N; i++ {
findFilesWithWalkDir(SearchPath)
}
}
func Benchmark_findFilesWithWalk(b *testing.B) {
SearchPath := os.ExpandEnv("${GOPATH}")
b.ResetTimer()
for i := 0; i < b.N; i++ {
findFilesWithWalk(SearchPath)
}
}
実行結果
$ go test -bench . -benchmem
goos: windows
goarch: amd64
pkg: walk_test
cpu: AMD Ryzen 7 3700X 8-Core Processor
Benchmark_findFilesWithWalkDir-16 1 2804402600 ns/op 166610536 B/op 1196620 allocs/op
Benchmark_findFilesWithWalk-16 1 10702168800 ns/op 316355376 B/op 1661835 allocs/op
PASS
ok walk_test 13.553s
- 結構な差が出ましたね
- 今回の実装の場合
filepath.WalkDir
のほうが
filepath.Walk
よりも以下の全てにおいていい結果が出ました- 実行時間
- アロケート容量
- アロケート回数
まとめ
- Go 1.16以上ででディレクトリ内を列挙する実装は
filepath.WalkDir
を使ったほうがいいようです-
filepath.Walk
のほうがいいケースがあるかもしれませんが、
自分は上記のような使い方を頻繁にするので
filepath.WalkDir
を積極的に使うようにします
-
Discussion