Goのファイル操作【プログラミング初心者向け教材】

プログラミング

プログラミングでは、何らかのデータを読み出したり、書き出したりすることが数多くあります。

データの保存先は、データベースが用いられることが一般的ですが、まとまった文章や構造化されていないデータなどはファイルで保存されることも多くあります。

そのため、ファイル操作はプログラミングを学習する中で重要項目の1つとなります。

本記事では、Goでよく使われるファイル操作について解説します。

プログラミング初心者の方の学習や、忘れてしまった方の復習として、参考にしていただければ幸いです。

記載しているプログラムは、Go1.15.2を使って動作確認をしています。

Goのファイル操作

Goのファイル操作として、以下の内容を採り上げます。

  • ファイル操作を扱うパッケージ
  • ファイルの読み込み
  • ファイルの書き込み
  • ファイルの削除
  • ファイルの存在確認
  • ファイルのコピー
  • ディレクトリの一覧取得
  • ディレクトリの作成
  • ディレクトリの削除

ファイル操作を扱うパッケージ

Goの基本的なファイル操作の多くは、os パッケージが提供する各関数で行います。

一括処理などを行う場合は、ioutil パッケージを使います。

効率よく読み書きするためのバッファリング処理が必要な場合には、bufio パッケージを使います。

ファイルの読み込み

ファイルを読み込むには、ioutilパッケージの ReadFile を使って読み込みます。

s, err := ioutil.ReadFile("./files/testfile.txt")
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(string(s))

上記のプログラムでは、filesディレクトリのtestfile.txtファイルを読み込んで、内容をコンソールに出力しています。
ファイルが存在しないなどにより、読み込みに失敗した場合は2つ目の戻り値として、エラー情報が返されます。

この方法では、ファイルの中身を一度にすべて読みこんでしまうため、大きなファイルだと時間がかかったり、メモリを大量に消費してしまいます。
ファイルを1行ずつ読み込むには、osパッケージのOpenでファイルを開き、bufioパッケージの Scan を使ってバッファリングしながら読み込みます。

file, err := os.Open("./files/testfile.txt")
if err != nil {
    fmt.Println(err)
    return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

上記のプログラムでは、for文で、Scanを呼出す毎に1行ずつ取得されます。

また、Openによってファイルを開いた場合は、クローズ処理を行う必要があります。
関数実行後にファイルクローズするために、deferを使ってクローズを行っています。

ファイルの書き込み

ファイルの書き込みは、新規にファイルを作成して書き込む方法と、既存のファイルに追記する方法があります。

新規ファイルへの書き込み

新規にファイルを作成して書き込むには、ioutilパッケージの WriteFile を使って書き込みます。
第3引数にはファイルのパーミッションを指定します。

text := "新規にファイルを作成するテスト"
err = ioutil.WriteFile("./files/writefile.txt", []byte(text), 0664)
if err != nil {
    fmt.Println(err)
    return
}

上記のプログラムでは、filesディレクトリにwritefile.txtというファイルを作成し、その中に「新規にファイルを作成するテスト」という文字列を書き込んでいます。
実行後にファイルが作成されますが、毎回新規にファイルを作成するため、何度実行してもファイルの中身は同じ結果になります。

既存ファイルへの追記

既存のファイルへ追記するには、osパッケージのOpenFileに追記モードを指定してファイルを開き、Fprintln を使って書き込みます。

text = "既存にファイルに追記するテスト"
file, err = os.OpenFile("./files/writefile.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
if err != nil {
    fmt.Println(err)
    return
}
defer file.Close()
fmt.Fprintln(file, text)

上記のプログラムでは、filesディレクトリのwritefile.txtというファイルに、「既存ファイルに追記するテスト」という文字列を追記しています。
オプションを指定することによって、既存ファイルが存在しない場合には新規にファイルを作成することもできます。

ファイルの削除

ファイルを削除するには、osパッケージの Remove を使います。

err = os.Remove("./files/writefile.txt")
if err != nil {
    fmt.Println(err)
    return
}

上記のプログラムでは、filesディレクトリのwritefile.txtというファイルを削除します。
ファイルが存在しない場合は、エラー情報が返されます。

ファイルの存在確認

ファイルが存在するかどうかを確認するには、osパッケージの Stat を使い、その結果で得られるエラー情報が、ファイルが存在しないことを表すエラーではないかどうかを確認します。

_, err = os.Stat("./files/testfile.txt")
fmt.Println(os.IsNotExist(err)) // -> false
_, err = os.Stat("./files/notexists.txt")
fmt.Println(os.IsNotExist(err)) // -> true
_, err = os.Stat("./files")
fmt.Println(os.IsNotExist(err)) // -> false

IsNotExistは、ファイルが存在しないエラーである場合はtrueを返し、そうでない場合はfalseを返します。
IsNotExistの結果がfalseとなる=ファイルが存在する、ということになります。
ディレクトリに対しても存在確認をすることができます。

ファイルかディレクトリかを判別するには、osパッケージの Stat で取得した情報の IsDir を使います。
ディレクトリである場合にはtrueを返し、そうでない場合はfalseを返します。

f, err := os.Stat("./files/testfile.txt")
fmt.Println(f.IsDir()) // -> false
f, err = os.Stat("./files")
fmt.Println(f.IsDir()) // -> true

ファイルのコピー

ファイルをコピーするには、ioパッケージの Copy を使います。
コピー先のファイルを Create で取得し、コピー元を Open で開いて、それぞれを Copy に渡します。

w, err := os.Create("./files/copyfile.txt")
if err != nil {
    fmt.Println(err)
    return
}
r, err := os.Open("./files/testfile.txt")
if err != nil {
    fmt.Println(err)
    return
}
_, err = io.Copy(w, r)
if err != nil {
    fmt.Println(err)
    return
}

上記のプログラムでは、filesディレクトリにcopyfile.txtを作成しますが、すでに同ファイルが存在している場合は、上書いてコピーします。

ディレクトリの一覧取得

ディレクトリのファイル一覧を取得するには、ioutilパッケージの ReadDir を使います。
ReadDirはディレクトリに存在するファイルとディレクトリの情報を配列で返します。

files, err := ioutil.ReadDir("./files/")
if err != nil {
    fmt.Println(err)
    return
}
for _, file := range files {
    fmt.Println(file.Name())
}

上記のプログラムでは、filesディレクトリ直下のファイルとディレクトリが取得され、その配下のファイルやディレクトリは取得されません。

配下のすべてのファイルやディレクトリを取得するには、filepathパッケージの Walk を使って走査します。

err = filepath.Walk("./files/", func(path string, info os.FileInfo, err error) error {
    if info.IsDir() {
        fmt.Println(info.Name())
        return nil
    }
    rel, err := filepath.Rel("./files/", path)
    fmt.Println(rel)
    return nil
})
if err != nil {
    fmt.Println(err)
    return
}

ディレクトリの作成

ディレクトリを新規に作成するには、osパッケージの Mkdir を使います。

err = os.Mkdir("./files/testdir1", 0755)
if err != nil {
    fmt.Println(err)
    return
}

上記のプログラムでは、filesディレクトリの配下にtestdir1ディレクトリを作成しています。
すでにtestdir1ディレクトリが存在する場合にはエラーとなります。

複数階層のディレクトリを一気に作成するには、osパッケージの MkdirAll を使います。
すでにディレクトリが存在している場合でもエラーになりません。

err = os.MkdirAll("./files/testdir2/testdir2", 0755)
if err != nil {
    fmt.Println(err)
    return
}

プログラムの中では、一時的にファイルを扱うためにディレクトリを作成したい場合があります。
プログラムの特性にもよりますが、基本的には他のプログラムや処理で使うディレクトリとは異なる専用のディレクトリを作るのが一般的です。
そのような用途の場合には、ランダムな名称でディレクトリを作成してくれる ioutilパッケージの TempDir を使います。

tempdir, err := ioutil.TempDir("./files/", "prefix-")
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(tempdir) // -> files/prefix-111682624

第1引数で作成するディレクトリを指定し、第2引数で接頭辞となる文字列を指定します。
戻り値として、作成したディレクトリ名を返します。

ディレクトリの削除

ディレクトリを削除するにはファイルの削除と同じく、osパッケージの Remove を使います。

err = os.Remove("./files/testdir1")
if err != nil {
    fmt.Println(err)
    return
}

削除対象のディレクトリに、ファイルやディレクトリが存在する場合はエラーとなります。

ディレクトリとファイルを一括で削除したい場合は、osパッケージの RemoveAll を使います。

err = os.RemoveAll("./files/testdir3")
if err != nil {
    fmt.Println(err)
    return
}

上記のプログラムでは、testdir3ディレクトリを配下のファイルやディレクトリとともに一括で削除します。
削除対象のディレクトリが存在しなくてもエラーにはなりません。

複数の処理で1つのファイル操作を行う場合は要注意

ファイルはデータを保存しておく場所として使いやすいのですが、複数のプロセスやスレッドなどから同じファイルを操作しようとすると思わぬことが発生することがあるので注意が必要です。

僕がこれまでに携わったプロジェクトでも以下のようなことがありました。

  • 複数のプロセスから同一のログファイルに書き込んだらファイルが壊れた
  • 参照しようとしたら別の処理が先に消していた
  • あるサーバが作成したファイルを別のサーバから参照しようとしたら、存在するはずのファイルが見えない状態だった

これらは、設計ミスやOSのファイル管理方法を理解出来ていないことが原因で発生したものでした。

ファイルはOSと密接に関わっているため、ローカル環境では上手く動いても、別の環境では意図通りに動かないといったことがあります。

ファイルを扱う際は、設計時点で問題ないかをきちんと議論することをおすすめします。

 

今回はGoのファイル操作について解説しました。

以上、参考になれば幸いです。

コメント

タイトルとURLをコピーしました