2
1

More than 3 years have passed since last update.

【Go】File operations (ファイル操作)

Last updated at Posted at 2021-03-28

Goでプログラミングの基礎を学ぶシリーズ

スクールでは教えてくれないプログラミングの基礎、データ構造とアルゴリズムをGoで学んでいくシリーズです。
そのデータ構造がどのようなものであるかは、理解を助けてくれるサイトを紹介しつつ、簡単に説明に留めさせていただきます。(ご自身でも調べてみてください!)
筆者自身、Exerciseに取り組みながら理解を深めていったので、コードの解説を中心に記事を書いていきたいと思います。

タイトル
#0 はじめに (環境構築と勉強方法)
#1 Pointer, Array, String (ポインタと配列と文字列と)
#2 File operations (ファイル操作) ☜ here
#3 Linked List (連結リスト)
#4 Stack & Queue (スタックとキュー)
#5 Search algorithms (探索アルゴリズム)
#6 Tree (木構造)
#7 Sorting algorithms (ソートアルゴリズム)
#8 String pattern matching algorithms (文字列探索アルゴリズム)

今回は#2 File operations (ファイル操作)です。

ファイル操作とは

読んで字の如く、ファイルを操作することです。
パソコンを触ったことがある人なら、マウスでファイルをクリックして開き、ファイルを編集して、ファイルを閉じたことが、一度はあると思います。
それです。
代表的な操作は以下の通りです。

Open ファイルを開く
Read ファイルを読み取る
Write ファイルに書き込む
Close ファイルを閉じる

Goのファイル操作に関して、英語ですが以下のサイトが参考になると思います。

コードを見たほうがわかりやすいと思いますので、早速Exerciseです。

💻 Exercise

事前にread.txtを作成し、適当な言葉を記述しておきます。
1. write.txtというファイルを作成しましょう。
2. read.txtに記述された言葉を一文字ずつ読み出しましょう。
3. read.txtから読み出した文字をwrite.txtに書き出しましょう。

☟ 解答例

▼ 事前に作成したファイル

read.txt
some
writes

▼ プログラム

package main

import (
    "fmt"
    "io"
    "os"
)

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {
    f, err := os.Open("read.txt")  // read.txtを開く
    check(err)

    defer f.Close() // read.txtを閉じる(f.Close()で詳説)

    fw, err := os.Create("write.txt") // write.txtという名前のファイルを作成
    check(err)

    defer fw.Close() // write.txtを閉じる

    b := make([]byte, 5) // 5byteの容量を持ったSlice

    // read.txtの中身を5byteずつ読み取り、読み取った分をwrite.txtに書き込む
    // read.txtに読み取るものがなくなるまで、5byteずつの読み書きを続ける
    for {
        n, err := f.Read(b) // read.txtの中身を5byte分読み取る
        if err != nil {
            if err != io.EOF {
                check(err)
            }
            break // read.txtに読み取るものが無くなったらbreakする(io.EOFで詳説)
        }
        fw.Write(b[:n]) // 読み取った分をwrite.txtに書き込む

        // log
        fmt.Println(n, b[:n])
        fmt.Println(" ", fmt.Sprintf("%c", b[:n]))
    }
}

// >log
// 5 [115 111 109 101 10]
//   [s o m e
// ]
// 5 [119 114 105 116 101]
//   [w r i t e]
// 2 [115 10]
//   [s
// ]

▼ 作成されたファイル

write.txt
some
writes

read.txtから5byteずつ読み取って、それをwrite.txtに書き出します。
logをご覧いただければわかると思いますが、改行も1文字として扱われます。

f.Close()

deferは、A Tour of Goによると、

defer ステートメントは、 defer へ渡した関数の実行を、呼び出し元の関数の終わり(returnする)まで遅延させるものです。
defer へ渡した関数の引数は、すぐに評価されますが、その関数自体は呼び出し元の関数がreturnするまで実行されません。

つまり、Exerciseのコードでは、write.txtへの書き込みが完了した後に、read.txtwrite.txtClose()しています。

なぜわざわざファイルを閉じる必要があるのかは、メモリの解放に関わっています。
ファイルはOpenするときにメモリを確保し、Closeするときに確保したメモリを解放します。
Closeしないと、使わなくなったメモリまでもが確保されたままになり、メモリの空きがなくなっていきます。
そのため、使わなくなるタイミングでCloseしてメモリを解放しておく必要があるのです。
参考: 【C言語】fopenしたならfcloseを忘れずにしっかりやりなさい。しないとどうなるのよ。

io.EOF

EOFは、End Of Fileの略であり、ファイルの終わりを意味します。
参考: Readers(A Tour of Go)
解答例のコードは以下のような流れで、読み取りを行っています。

    for {
        n, err := f.Read(b)    // read.txtファイルを5byteずつ読み取る
        if err != nil {        // 読み取りでエラーがあった場合
            if err != io.EOF { // そのエラーがEOFでなかった場合
                check(err)     // panic!
            }
            break // そのエラーがEOFの場合、読み取るものがないということなので、breakする
        }
        // 省略
    }

では、ファイルの中身の読み取り方と、読み取るものを少し変えてみましょう。

💻 Exercise

一つ前のExerciseでは一文字ずつ読み取りましたが、次は一行ずつ読み取りましょう。
ついでに、行数も出力しましょう。
例えば、事前に用意したファイルが

text.txt
This is sample file.
Have a nice day!

の場合、出力は以下のようになります。

1 This is sample file.
2 Have a nice day!

☟ 解答例

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

func check(e error) {
    if e != nil {
        if e != io.EOF {
            panic(e)
        }
    }
}

func main() {
    f, err := os.Open("text.txt")
    check(err)

    defer f.Close()

    r := bufio.NewReader(f) // Readerを初期化する

    var lines []string // 読み取ったStringを格納しておくSlice
    for {
        l, err := r.ReadString('\n') // 区切り文字である改行コード('\n')まで読み取る

        lines = append(lines, l) // 読み取ったStringをSliceに追加
        if err == io.EOF {
            break
        }
        check(err)
    }

    for i, l := range lines {
        fmt.Printf("%d %s", i+1, l)
    }
}

// >result
// 1 This is sample file.
// 2 Have a nice day!

💻 Exercise

次は構造体の読み書きをしてみましょう。
名前、電話番号、メールアドレスが記載されたアドレス帳を想定します。
アドレスが記載されたファイルから内容を読み取り、その中身をコピーしたファイルを作成します。
fread()でファイルから読み取り、fwrite()でファイルに書き込むプログラムを作成しましょう。

☟ 解答例

▼ 事前に作成したファイル

address.txt
Sakura
090-1234-5678
sakura@example.com

Kaede
080-1234-5678
kaede@example.com

Yuzu
070-1234-5678
yuzu@example.com

▼ プログラム

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

type Address struct {
    name             string
    telephone_number string
    email_address    string
}

func check(e error) {
    if e != nil {
        if e != io.EOF {
            panic(e)
        }
    }
}

func fread(file string) []*Address {
    f, err := os.Open(file)
    check(err)

    defer f.Close()

    r := bufio.NewReader(f)
    var data []*Address
    for {
        n, err := r.ReadString('\n') // nameの読み取り
        check(err)
        t, err := r.ReadString('\n') // telephone_numberの読み取り
        check(err)
        e, err := r.ReadString('\n') // email_addressの読み取り
        check(err)

        address := &Address{name: n, telephone_number: t, email_address: e}
        data = append(data, address)

        _, err = r.ReadString('\n')
        if err == io.EOF {
            break
        }
        check(err)
    }

    return data
}

func fwrite(file string, data []*Address) {
    f, err := os.Create(file)
    check(err)

    defer f.Close()

    for _, v := range data {
        f.WriteString(fmt.Sprintf("%s%s%s\n", v.name, v.telephone_number, v.email_address))
    }
}

func main() {
    data := fread("address.txt")
    fwrite("addressCopy.txt", data)
}

▼ 作成されたファイル

addressCopy.txt
Sakura
090-1234-5678
sakura@example.com

Kaede
080-1234-5678
kaede@example.com

Yuzu
070-1234-5678
yuzu@example.com


main()がかなりすっきりしました。
fread()では、読み取るファイル名を引数に取り、読み取ったaddressを配列に格納して返しています。
fwrite()では、新たに作成するファイル名とfread()で返ってきたアドレスの配列を引数に取り、fwrite()内で作成されたファイルへの書き込みを行っています。

途中に出てくるポインタ([]*Addressとか&Address{})がわからないよ!という方は、前の記事を参考にしてみてください。
参考: 【Go】Pointer, Array, String (ポインタと配列と文字列と)

おわりに

Exerciseの解答例はあくまで例なので、もっといい書き方あるよ!という方、ぜひコメントをお寄せください!
説明についても、筆者自身が初心者であるため、ご指摘や補足は大歓迎でございます。

株式会社Link Sportsでは、あらゆるスポーツを楽しむ人たちに送る、チームマネジメントアプリを開発しています。
未経験でも経験豊富なエンジニアの方でも、スポーツ好きなら活躍できる環境があります!
絶賛エンジニア募集中です!
Wantedly ▶︎ https://www.wantedly.com/projects/324177
Green ▶︎ https://www.green-japan.com/job/82003

次回は、データ構造とアルゴリズム#3 Linked List (連結リスト)です。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1