kurumi-bioの雑記帳

プログラミング、パソコン、ペット、 犬、お出かけ

初心者のGo言語 -47- <VolumeName,Walk,WalkDir,WalkFunc>

こんにちは、kurumi-bioです。
第5回目のfilepathパッケージ(標準ライブラリー)の学習です。

前回の記事

kurumi-bio.hatenablog.com

APIリファレンス(過去記事の一覧)

kurumi-bio.hatenablog.com

環境

  • Windows
    OSバージョン:Windows11 Home 22H2
    Go言語のバージョン:go version go1.20.3 windows/amd64
  • Linux
    OSバージョン:openSUSE Leap 15.4
    Go言語のバージョン:go version go1.20.3 linux/amd64

VolumeName関数

func VolumeName(path string) string

関数の説明 VolumeName は先頭のボリューム名を返します。
「C:\foo\bar」と指定すると、Windows では「C:」が返されます。
"\host\share\foo" を指定すると、"\host\share" が返されます。
他のプラットフォームでは「」を返します。

package main

import (
    "fmt"
    "path/filepath"
)

func printResult(p string) {
    v := filepath.VolumeName(p)
    fmt.Printf("\tpath=[%-25s] - valumeName[%-15s]\n", p, v)
}

func main() {
    fmt.Println("◆ドライブ名有り")
    printResult("c:\\test1\\test.txt")
    printResult("c:/test1/test.txt")
    fmt.Println("◆UNC形式")
    printResult("\\\\test1\\test2\\test.txt")
    printResult("//test1/test2/test.txt")
    fmt.Println("◆ドライブ名無し")
    printResult("\\test1\\test.txt")
    printResult("/test1/test.txt")
}

コードの説明 ファイルパス文字列の操作を行う関数ですので、色々な形式のパスを渡して結果を確認してみました。

◆実行結果(Windows)

◆実行結果(Linux)

実行結果の説明 Windows環境ではボリューム名が返されましたが、Linux環境では、空になりました。
パスにドライブ名が記載されている場合は、ドライブ名が返されて、
UNCパスの場合は、コンピューター名と共有フォルダ名までがボリューム名として返されました。
文字列操作ですので、パスのフォルダが存在しなくてもエラーになりませんでした。

Walk関数

func Walk(root string, fn WalkFunc) error

関数の説明 Walk は、ルートをルートとしてファイル ツリーをたどり、ルートを含むツリー内の各ファイルまたはディレクトリに対して fn を呼び出します。

ファイルおよびディレクトリにアクセスする際に発生するすべてのエラーは、fn によってフィルタリングされます。詳細については、WalkFunc のドキュメントを参照してください。

ファイルは字句順にウォークされるため、出力は決定的になりますが、ディレクトリのウォークを開始する前に、Walk がディレクトリ全体をメモリに読み取る必要があります。

Walk はシンボリック リンクをたどりません。

Walk は、Go 1.16 で導入された WalkDir よりも効率が低く、アクセスしたすべてのファイルまたはディレクトリに対して os.Lstat を呼び出すことが回避されます。

package main

import (
    "fmt"
    "io/fs"
    "path/filepath"
)

func skipDirFunc(path string, info fs.FileInfo, err error) error {
    if err != nil {
        fmt.Printf("skipDirFunc Error:%v\n", err)
        return err
    }
    if info.Name() == "Skip" {
        fmt.Printf("\tSkip [%s]\n", path)
        return filepath.SkipDir
    }

    fmt.Printf("\t%s\n", path)
    return nil
}

func skipAllFunc(path string, info fs.FileInfo, err error) error {
    if err != nil {
        fmt.Printf("skipAllFunc Error:%v\n", err)
        return err
    }
    if info.Name() == "Skip" {
        fmt.Printf("\tSkipAll [%s]\n", path)
        return filepath.SkipAll
    }
    fmt.Printf("\t%s\n", path)
    return nil
}

func printResult(r string) {
    fmt.Println("◆All")
    e := filepath.Walk(r, func(path string, info fs.FileInfo, err error) error {
        if err != nil {
            fmt.Printf("Walk Error:%v\n", err)
            return err
        }
        fmt.Printf("\t%s\n", path)
        return nil
    })
    if e != nil {
        fmt.Println(e)
    }

    fmt.Println("◆SkipDir")
    filepath.Walk(r, skipDirFunc)
    fmt.Println("◆SkipAll")
    filepath.Walk(r, skipAllFunc)
}

func main() {
    printResult("a")
}

コードの説明 指定したフォルダの以下のファイル/フォルダに対してWalkFunc型の関数を呼び出します。
Go言語では、関数の引数に関数を記載することが可能です。
記載は、関数の処理をそのまま記載する方法と関数名を渡す方法がありますので両方試してみました。
1つ目の処理(◆All)は、aフォルダ配下のパスを出力しています。
2つ目の処理(◆SkipDir)は、aフォルダ配下でSkipという名前があったらそのフォルダの処理をスキップします。
3つ目の処理(◆SkipDir)は、aフォルダ配下でSkipという名前があったら、処理を中断するようにしています。
詳細は、型定義WalkFuncの説明に記載されています。

◆実行結果(Windows)

◆実行結果(Linux)

実行結果の説明 Windows環境とLinux環境で同じ結果になりました。
◆Allは、全てのファイルパスが出力されました。
◆SkipDirは、Skipフォルダ以下の階層が出力されませんでした。
◆SkipAllは、Skipフォルダ以降のファイルパスが出力されませんでした。

WalkDir関数

func WalkDir(root string, fn fs.WalkDirFunc) error

関数の説明 WalkDir は、ルートをルートとしてファイル ツリーをたどり、ルートを含むツリー内の各ファイルまたはディレクトリに対して fn を呼び出します。

ファイルおよびディレクトリへのアクセス時に発生するすべてのエラーは、fn によってフィルタリングされます。詳細については、fs.WalkDirFunc ドキュメントを参照してください。

ファイルは字句順に探索されるため、出力は確定的になりますが、ディレクトリの探索を開始する前に、WalkDir がディレクトリ全体をメモリに読み込む必要があります。

WalkDir はシンボリック リンクをたどりません。

WalkDir は、オペレーティング システムに適切な区切り文字を使用するパスを指定して fn を呼び出します。これは、常にスラッシュで区切られたパスを使用する io/fs.WalkDir とは異なります。

package main

import (
    "fmt"
    "io/fs"
    "path/filepath"
)

func skipDirFunc(path string, d fs.DirEntry, err error) error {
    if err != nil {
        fmt.Printf("skipDirFunc Error:%v\n", err)
        return err
    }
    if d.Name() == "Skip" {
        fmt.Printf("\tSkipDir [%s]\n", path)
        return filepath.SkipDir
    }
    fmt.Printf("\t%s\n", path)
    return nil
}

func skipAllFunc(path string, d fs.DirEntry, err error) error {
    if err != nil {
        fmt.Printf("skipAllFunc Error:%v\n", err)
        return err
    }
    if d.Name() == "Skip" {
        fmt.Printf("\tSkipAll [%s]\n", path)
        return filepath.SkipAll
    }
    fmt.Printf("\t%s\n", path)
    return nil
}

func printResult(r string) {
    fmt.Println("◆All")
    e := filepath.WalkDir(r, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            fmt.Printf("WalkDir Error:%v\n", err)
            return err
        }
        fmt.Printf("\t%s\n", path)
        return nil
    })

    if e != nil {
        fmt.Println(e)
    }

    fmt.Println("◆SkipDir")
    filepath.WalkDir(r, skipDirFunc)
    fmt.Println("◆SkipAll")
    filepath.WalkDir(r, skipAllFunc)
}

func main() {
    printResult("a")
}

コードの説明 filepath.Walk()関数と同じですが、第二引数の関数がfs.WalkDirFuncに変わっています。
そのため、取得できる内容も若干異なります。

◆実行結果(Windows)

◆実行結果(Linux)

実行結果の説明 filepath.Walk()関数と同じ結果になりました。

型定義 WalkFunc

type WalkFunc func(path string, info fs.FileInfo, err error) error

関数の説明 WalkFunc は、各ファイルまたはディレクトリにアクセスするために Walk によって呼び出される関数のタイプです。

path 引数には、接頭辞として Walk への引数が含まれます。
つまり、Walk がルート引数 "dir" で呼び出され、そのディレクトリ内で "a" という名前のファイルが見つかった場合、walk 関数は引数 "dir/a" で呼び出されます。

ディレクトリとファイルは Join で結合され、ディレクトリ名が消去される可能性があります。
Walk がルート引数 "x/../dir" で呼び出され、そのディレクトリ内で "a" という名前のファイルが見つかった場合、walk 関数が呼び出されます。
引数は「x/../dir/a」ではなく「dir/a」です。

info 引数は、名前付きパスの fs.FileInfo です。

関数によって返されるエラー結果は、Walk の続行方法を制御します。
関数が特別な値 SkipDir を返す場合、Walk は現在のディレクトリ (info.IsDir() が true の場合はパス、それ以外の場合はパスの親ディレクトリ) をスキップします。
関数が特別な値 SkipAll を返した場合、Walk は残りのファイルとディレクトリをすべてスキップします。
それ以外の場合、関数が非 nil エラーを返した場合、Walk は完全に停止し、そのエラーを返します。

err 引数はパスに関連するエラーを報告し、Walk がそのディレクトリに入らないことを示します。
関数はそのエラーを処理する方法を決定できます。
前述したように、エラーを返すと、Walk はツリー全体の歩行を停止します。

Walk は 2 つの場合に、非 nil err 引数を指定して関数を呼び出します。

まず、ルート ディレクトリまたはツリー内の任意のディレクトリまたはファイルの os.Lstat が失敗した場合、Walk はパスをそのディレクトリまたはファイルのパスに設定し、info を nil に設定し、err を os.Lstat からのエラーに設定して関数を呼び出します。 。

次に、ディレクトリの Readdirnames メソッドが失敗した場合、Walk は path をディレクトリのパスに設定し、info をディレクトリを記述する fs.FileInfo に設定し、err に Readdirnames からのエラーを設定して関数を呼び出します。

最後に

関数の引数に関数を使えるのは、色々と工夫できそうな感じがします。
引数を同じにすれば、別名で実装できますので、場合分けして使うことができそうです。
とはいえ、普通に関数内で呼び出すのと変わらない気がしますが、きっと何か意味があるのだと思います。
Go言語を使いこなせるようになれば、違いが判るのかと思います。

最後までご覧いただきありがとうございます