Home About Contact
Go

Go, listFiles 指定のディレクトリ内のファイルまたはディレクトリを列挙する(改良版の改良版)

Go で listFiles する方法(改良版) の改良版。

何をどう改良したいかというと、 たとえば Groovy で記述した場合・・・

例1:

def fileProc = { file->
    if( file.isFile() ){
        println(file.name)
    }
}

new File("posts").listFiles().each( fileProc )

またはさらに簡単に例2:

new File("posts").listFiles().each { file->
    if( file.isFile() ){
        println(file.name)
    }
}

のように記述できる。 Go でもこのように記述したい。

前回書いたコードそのままですが、 出発点となる改良対象のコードを掲載。

package main

import (
    "os"
    "fmt"
)

type MyFile struct {
    Name string
    Dir  bool
    File bool
}

func listFiles(pathName string) (*[]MyFile, error) {
    f, err := os.Open(pathName)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    myFiles := []MyFile{}

    fileInfos, err := f.Readdir(0)
    for _, fileInfo := range fileInfos {
        dir := fileInfo.IsDir()
        myFiles = append(myFiles, MyFile{Name: fileInfo.Name(), Dir: dir, File: !dir})
    }

    return &myFiles, nil
}

func main(){
    postsDir := "posts"
    files, err := listFiles(postsDir)
    if err == nil {
        for _, file := range *files {
            if !file.Dir {
                fmt.Println(file.Name)
            }
        }
    }
}

それでは、このコードを修正して、冒頭の Groovy のコードと同じように記述できる形に変えます。

プロジェクトの準備

環境を確認:

$ go version
go version go1.21.1 linux/amd64

プロジェクトディレクトリを作成して準備:

$ mkdir listfiles
$ cd listfiles
$ go mod init listfiles
$ touch main.go

それではこの main.go にコードを書いていきます。

見つけたファイルに対して何らかの処理をする関数を定義

今回のテーマ部分、ファイルのリストを得てその後なんやかんやするのではなく、ファイルリストを取得中にファイルを見つけたら処理する 関数タイプ を定義。

タイプを定義:

type MyFileProc = func(MyFile)

Golang の場合、kotlin の Unit に相当する記述がないらしいです。(軽く調べた限りでは。) 少し気持ち悪いのですが仕方ありません。

以下のように明確に定義したいところ(kotlinで同種のものを定義した場合)。

typealias MyFileProc = (MyFile)->Unit

もう定義は以下のように簡潔にかければいいのに。

MyFileProc :: MyFile->Unit

この関数タイプ(シグニチャ)を持つ関数の実装を用意しておきます。

myFileProc := func(file MyFile) {
    if !file.Dir {
        fmt.Println(file.Name)
    }
}

listFiles の修正

関数 listFiles を修正して、対象パスだけでなく、関数タイプ MyFileProc を引数にとること、 そして、以前は MyFile リストの参照 を返していましたが、それをやめて、error のみ返すようにしました。

func listFiles(pathName string, cl MyFileProc) error {
    // これから実装する.
}

実装の中身はこれ:

func listFiles(pathName string, cl MyFileProc) error {
    f, err := os.Open(pathName)
    if err != nil {
        return err
    }
    defer f.Close()

    fileInfos, err := f.Readdir(0)
    for _, fileInfo := range fileInfos {
        dir := fileInfo.IsDir()
        myFile := MyFile{Name: fileInfo.Name(), Dir: dir, File: !dir}
        cl(myFile)
    }

    return nil
}

以前のコードでは、得られたファイルまたはディレクトリを用意したリストに蓄積していましたが、 修正したコードでは、見つけ次第 cl (MyFileProc) を call して(適用してというべきか)処理をそちらにまかせています。

あとは、この 関数 listFiles を使うだけです。

myFileProc := func(file MyFile) {
    if !file.Dir {
        fmt.Println(file.Name)
    }
}

postsDir := "posts"
listFiles(postsDir, myFileProc)

もちろん、 MyFileProc の内容がこの例のように単純ならば、 上記コードのように冗長に書く必要はない。 以下のようにコンパクトに書けます。

listFiles(postsDir, func(file MyFile) {
    if !file.Dir {
        fmt.Println(file.Name)
    }
})

または、この関数の内容が複雑なので・・・などの理由により、もっと冗長に書きたければ次のように書くこともできる。

func myComplexFileProc(file MyFile) {
    if !file.Dir {
        fmt.Println(file.Name)
    }
}

func main(){
    postsDir := "posts"
    listFiles(postsDir, myComplexFileProc)
}

ここまでのコード

main.go

package main

import (
    "os"
    "fmt"
)

type MyFile struct {
    Name string
    Dir  bool
    File bool
}

// MyFileProc :: MyFile->Unit
type MyFileProc = func(MyFile)

func listFiles(pathName string, cl MyFileProc) error {
    f, err := os.Open(pathName)
    if err != nil {
        return err
    }
    defer f.Close()

    fileInfos, err := f.Readdir(0)
    for _, fileInfo := range fileInfos {
        dir := fileInfo.IsDir()
        myFile := MyFile{Name: fileInfo.Name(), Dir: dir, File: !dir}
        cl(myFile)
    }

    return nil
}

func main(){
    fileProc := func(file MyFile) {
        if !file.Dir {
            fmt.Println(file.Name)
        }
    }

    postsDir := "posts"
    listFiles(postsDir, fileProc)

    /*
    listFiles(postsDir, func(file MyFile) {
        if !file.Dir {
            fmt.Println(file.Name)
        }
    })
    */
}

以下で実行します。

$ go run main.go

この関数に対して Closure 機能を使うことで振る舞いを変更する

このコードでは、./posts/ 以下にファイルが存在していることを前提にしているので、 いま以下のようになっていることにします。

$ ls ./posts
aaa.txt  bbb.md  ccc.md

そして特定の拡張子 md / txt を持つファイルのみ標準出力する MyFileProc をそれぞれつくることにします。それらの関数内にその判定器 FileDetection をキャプチャさせます。(encloseする)。

判定器の関数タイプを定義。

// FileDetector :: MyFile->bool
type FileDetector = func(MyFile) bool

任意に種類のファイルだけを列挙(標準出力)する MyFileProc Clsoure を作り出す関数:

func toMyFileProc(detector FileDetector) MyFileProc {
    return func(file MyFile) {
        if detector(file) {
            fmt.Println(file.Name)
        }
    }
}

上記を使って listFiles する。 まず、md 拡張子を持つファイルのみ標準出力する例:

mdFileDetector := func(file MyFile) bool {
    return (!file.Dir && strings.HasSuffix(file.Name, "md"))
}

mdFileProc := toMyFileProc(mdFileDetector)

postsDir := "posts"
listFiles(postsDir, mdFileProc)

go run main.go して、bbb.md, ccc.md だけが標準出力されるのを確認しましょう。

そして、txt 拡張子を持つファイルだけを標準出力する例:

txtFileDetector := func(file MyFile) bool {
    return (!file.Dir && strings.HasSuffix(file.Name, "txt"))
}

txtFileProc := toMyFileProc(txtFileDetector)

postsDir := "posts"
listFiles(postsDir, txtFileProc)

まとめ

最終的にできたコードを掲載します。

main.go

package main

import (
    "os"
    "fmt"
    "strings"
)

type MyFile struct {
    Name string
    Dir  bool
    File bool
}

// MyFileProc :: MyFile->Unit
type MyFileProc = func(MyFile)

func listFiles(pathName string, cl MyFileProc) error {
    f, err := os.Open(pathName)
    if err != nil {
        return err
    }
    defer f.Close()

    fileInfos, err := f.Readdir(0)
    for _, fileInfo := range fileInfos {
        dir := fileInfo.IsDir()
        myFile := MyFile{Name: fileInfo.Name(), Dir: dir, File: !dir}
        cl(myFile)
    }

    return nil
}

// FileDetector :: MyFile->bool
type FileDetector = func(MyFile) bool

func toMyFileProc(detector FileDetector) MyFileProc {
    return func(file MyFile) {
        if detector(file) {
            fmt.Println(file.Name)
        }
    }
}

func main(){
    postsDir := "posts"

    // md
    mdFileDetector := func(file MyFile) bool {
        return (!file.Dir && strings.HasSuffix(file.Name, "md"))
    }

    mdFileProc := toMyFileProc(mdFileDetector)
    listFiles(postsDir, mdFileProc)

    // txt
    txtFileDetector := func(file MyFile) bool {
        return (!file.Dir && strings.HasSuffix(file.Name, "txt"))
    }

    txtFileProc := toMyFileProc(txtFileDetector)
    listFiles(postsDir, txtFileProc)
}

以上です。