Go 言語のいろいろなパッケージを使ってみるシリーズ(目次)。 今回も os パッケージの os.File 関連ですが、*os.File に定義されているメソッドを見ていきます。
Linux コマンドに同名のものがある Chdir, Chmod, Chown は Windows ではサポートされておらずエラーが返されるので、ここでは扱いません。 まぁ特に使い方は難しくないと思いますが*1。
【この記事の内容】
*os.File に定義されているメソッド
os パッケージのパッケージドキュメントはこちら。 *os.File に定義されているメソッドには以下のようなものがあります:// ファイル情報 func (f *File) Name() string func (f *File) Fd() uintptr func (f *File) Stat() (FileInfo, error) // 読み書き(ファイル) func (f *File) Read(b []byte) (n int, err error) func (f *File) ReadAt(b []byte, off int64) (n int, err error) func (f *File) Write(b []byte) (n int, err error) func (f *File) WriteAt(b []byte, off int64) (n int, err error) func (f *File) WriteString(s string) (n int, err error) func (f *File) Seek(offset int64, whence int) (ret int64, err error) func (f *File) Close() error // ディレクトリ操作 func (f *File) Readdir(n int) ([]FileInfo, error) func (f *File) Readdirnames(n int) (names []string, err error) // その他 func (f *File) Truncate(size int64) error func (f *File) Sync() error // Linux コマンド系のメソッド(Windows ではサポートされていない) func (f *File) Chdir() error func (f *File) Chmod(mode FileMode) error func (f *File) Chown(uid, gid int) error
ファイル情報
まずはファイル情報を取得したり変更したりするメソッドを見ていきましょう。 サンプルコードでは簡単のためファイルを閉じる部分を省略しています。ファイル名とファイル記述子(Name, Fd メソッド)
ファイル名とファイル記述子*2は os.File オブジェクトから直接取得できます:f, err := os.Create("hello-world.txt") if err != nil { log.Fatal(err) } fmt.Printf("ファイル名:%s\n", f.Name()) fmt.Printf("ファイル記述子:%d\n", f.Fd()) // Output: // ファイル名:hello-world.txt // ファイル記述子:452
ファイル記述子の値は実行ごとに異なりえます。
ファイル情報(Stat メソッド)
もう少し詳しいファイル情報を取得したい場合は、Stat メソッドによって os.FileInfo オブジェクトを取得し、さらに必要なプロパティを取得します。 os.FileInfo 型は以下のように定義されています:// ファイル情報 type FileInfo{ Name() string Size() int64 Mode() FileMode ModTime() time.Time IsDir() bool Sys() interface{} }
これらを実際に使ってファイル情報を取得してみると
// ファイル作成 f, err := os.Create("hello-world.txt") if err != nil { log.Fatal(err) } f.WriteString("Hello, world!") // ファイル情報の取得 info, err := f.Stat() if err != nil { log.Fatal(err) } fmt.Printf("ファイル名:%s\n", info.Name()) fmt.Printf("サイズ:%d\n", info.Size()) fmt.Printf("ファイルモード:%v\n", info.Mode()) fmt.Printf("最終更新日時:%v\n", info.ModTime()) fmt.Printf("ディレクトリ?:%v\n", info.IsDir()) // Output: // ファイル名:hello-world.txt // サイズ:13 // ファイルモード:-rw-rw-rw- // 最終更新日時:2017-10-03 10:55:22.4958144 +0900 JST // ディレクトリ?:false
- Stat メソッドは第2返り値としてエラーを返します(微妙に面倒くさい)。
- Size メソッドはファイルの(バイト)サイズを返します。 今の場合、"Hello, world!" の13です。
- Mode メソッドは前回も出てきた FileMode (実質 uint32 のビットフラグ)を返します。 FileMode には String メソッドが定義されていて、POSIX パーミッション「-rw-rw-rw」が表示されています(最初の「-」はディレクトリなら "d" などが表示されます)。
- Sys メソッドは使っていませんが、OS 依存の何らかのオブジェクトが返されます。
FileMode は実質 uint32 のビットフラグですが、いくつか便利なメソッドが定義されています:
// ファイルモード type FileMode uint32 func (m FileMode) IsRegular() bool func (m FileMode) IsDir() bool func (m FileMode) Perm() FileMode func (m FileMode) String() string
ファイルがディレクトリかどうかを見るには、この IsDir 関数を使えばよさそうです(Stat が第2返り値にエラーを返すので1行で書けないツラさ...)。
読み書き(ファイル)
次はファイル内容の読み書きを行うメソッドを見ていきます。 Close メソッドはファイルを閉じるメソッドで、ファイルを使用した後に呼び出してリソースを解放する必要があります。 エラーが発生しても閉じられるように defer 文とともに使っておくのが無難でしょう。読み込み Read, ReadAt メソッド
Read メソッドは指定したバイトスライスにファイル内容を読み込みます。 ファイル内容が充分にあれば読み込まれるバイト数は渡したスライスの長さとなり、なければ残りの内容すべてとなります。 このとき、int の第1返り値で読み込んだバイト数を得られます。 第2返り値は読み込み時のエラーを返し、ファイル末尾まで達して読み込めるバイトが0の場合は io.EOF が返されます。以下のサンプルコードでは、「hello-world.txt」というファイルがあり、内容が 「Hello, world!」(13バイト)となっているとします。 このとき
f, err := os.Open("hello-world.txt") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 // Read メソッド buf := make([]byte, 10) // 10 バイトごとに読み込む f.Read(buf) fmt.Print(string(buf)) // 「Hello, wor」(10バイト分)と表示 // ファイル内容が充分にない場合 n, err := f.Read(buf) if err != nil { log.Fatal(err) } if n != len(buf) { fmt.Print(string(buf[:n])) // 「ld!」と表示(n = 3) } // 読み込めるファイル内容がない場合 n, err = f.Read(buf) fmt.Println(n) // 「0」と表示 fmt.Println(err) // io.EOF
ReadAt メソッドは指定した位置からファイル内容を読み込みます。 ファイル内容が充分になければ読み込んだバイト数が0でなくても io.EOF が返されるので注意(Read 関数は読み込んだバイト数が0のときだけ io.EOF を返した)。
f, err := os.Open("hello-world.txt") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 // ReadAt メソッド buf := make([]byte, 10) f.ReadAt(buf, 2) fmt.Print(string(buf)) // 「llo, world」と表示(位置2から10バイト読み込み) // ファイル内容が充分にない場合 n, err := f.ReadAt(buf, 9) if err == io.EOF { fmt.Print(string(buf[:n])) // 「rld!」と表示(n = 4) }else if err != nil { log.Fatal(err) }
指定する位置は既に読み込んだ箇所でもかまいません。
書き出し Write, WriteAt, WriteString メソッド
Write 関数は指定したバイトスライスをファイルに書き出します:f, err := os.Create("hello-world.txt") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 // Write メソッド content := []byte("Hello, world!\n") f.Write(content) // 返り値は書き込んだバイト数とエラー n, err := f.Write([]byte("Hello, world2!\n")) fmt.Println(n, err) // 「15 <nil>」
第1返り値の int 値は書き込まれたバイト数です。 バイト数が引数のバイトスライスの長さと異なる場合は nil でないエラーが一緒に返されます。
WriteAt メソッドは指定した位置から内容を書き込みます:
f, err := os.Create("hello-world.txt") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 f.Write([]byte("Hello, world!\n")) // WriteAt メソッド n, err := f.WriteAt([]byte("Hello, world2!\n"), 7) fmt.Println(n, err) // 「15 <nil>」
これを実行するとファイル内容は
Hello, Hello, world2!
のように途中から上書きされます。 指定した位置がファイルサイズを超えている場合は、元のファイル内容の直後から書き込みが行われます:
f, err := os.Create("hello-world.txt") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 f.Write([]byte("Hello, world1!\n")) // 指定した位置が元のファイルサイズを超える場合 f.WriteAt([]byte("Hello, world2!\n"), 100)
これを実行すると
Hello, world1! Hello, world2!
のように元の内容に続いて書き込みが行われます。
WriteString メソッドはバイトスライスではなく文字列を渡してファイル内容を書き込みます:
f, err := os.Create("hello-world.txt") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 // WriteString メソッド f.WriteString("Hello, world!\n") // 返り値は書き込んだバイト数とエラー n, err := f.WriteString("Hello, world2!\n") fmt.Println(n, err) // 「15 <nil>」
Seek メソッド
Seek メソッドは次に読み書きする位置を設定します。 第2引数から決まる位置から第1引数で指定されるオフセット分移動した位置から次の読み書きを行います。 第2引数で決まる位置は io パッケージの定数を使います:- 第2引数が io.SeekStart (== 0) → ファイルの先頭
- 第2引数が io.SeekCurrent (== 1) → 現在の読み書き位置
- 第2引数が io.SeekEnd (== 2) → ファイルの末尾
となります。 使ってみた方がまだ少しは分かりやすいかと思います:
f, err := os.Create("hello-world.txt") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 f.WriteString("Hello, world!") // ファイル内容は「Hello, world!」 // Seek メソッド(1回目) ret, _ := f.Seek(7, io.SeekStart) // 先頭から数えて7、つまり位置7 fmt.Println(ret) // 「7」 f.WriteString("Hello, world1!") // 位置7から書き込み // ファイル内容は「Hello, Hello world1!」 // 書き込み後は位置21 // Seek メソッド(2回目) ret, _ = f.Seek(-7, io.SeekEnd) // 末尾(21)から-7、つまり位置14 fmt.Println(ret) // 「14」 f.WriteString("Hello, world2!") // 位置14から書き込み // ファイル内容は「Hello, Hello, Hello world2!」 // 書き込み後は位置27
まぁ、あまり分かりやすくはないですかね・・・ 自分でコードを書いてみればそんなに複雑なことはしてない感じがしまが。
*os.File が実装している io パッケージのインターフェース
この節で扱ったメソッドによって、*os.File が io パッケージのいろいろなインターフェースを実装します。 io パッケージに定義されているインターフェースについては「Go 言語の io パッケージに定義されているインターフェース型を一気見する」参照。まず、Read, Write, Seek, Close メソッドによってバイトスライスの入出力に関するインターフェースは全て実装しています:
- io.Reader, io.Writer, io.Seeker, io.Closer
- io.ReadWriter, io.ReadSeeker, io.ReadCloser, io.WriteSeeker, io.WriteCloser
- io.ReadWriteSeeker, io.ReadWriteCloser
また、ReadAt, WriteAt メソッドによって以下の2つのインターフェースを実装しています:
- io.ReaderAt
- io.WriterAt
読み込み(ディレクトリ)
次はディレクトリに対して格納されているファイルの情報を読み取るメソッド。- Readdirnames メソッド
- Readdir メソッド
の2つがあります。
サンプルとして以下のようなディレクトリ構成があるとします:
- hello
- hello-world1.txt
- hello-world2.txt
Readdirname メソッド
Readdirname メソッドはファイル名のスライスを返します:dir, err := os.Open("hello") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 // 一応、ディレクトリであることを確認(特に必要なし) dirInfo, _ := dir.Stat() fmt.Println(dirInfo.IsDir()) // 「true」 // Readdirnames メソッド fns, err := dir.Readdirnames(-1) // 全てのファイルを読み出す if err != nil { log.Fatal(err) } for _, fn := range fns { fmt.Println(fn) } // Output: // hello-world1.txt // hello-world2.txt
引数は読み込む最大のファイル数です。 -1を指定すると全てのファイルを読み出します。 読み込んだファイルがあれば、指定したファイル数に達しなくても読み込んだ結果のスライスを返し、エラーは返されません。 読み込みが最後まで達して読み込むファイルがなかった場合は io.EOF が返されます。
Readdir メソッド
Readdir メソッドはもう少し詳しいファイル情報 (FileInfo) のスライスを返します:dir, err := os.Open("hello") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 // Readdir メソッド infos, err := dir.Readdir(-1) if err != nil { log.Fatal(err) } for _, info := range infos { fmt.Printf("%s: %d\n", info.Name(), info.Size()) } // Output: // hello-world1.txt: 15 // hello-world2.txt: 15
引数や第2返り値のエラーに関しては Readdirnames メソッドと同じです。
これらのメソッドの使い方は特に難しいわけではありませんが、ioutil.ReadDir 関数を使えば os.Open などで *io.File を取得しなくても簡単にディレクトリの内容を読み込むことができます(Readdir メソッドと同じく FileInfo のスライスを返す)。
その他
最後は残りのメソッドをまとめて見ていきます。Truncate メソッド
Truncate メソッドは指定したサイズにファイルを切り詰めます:f, err := os.Create("hello-world.txt") if err != nil { log.Fatal(err) } defer f.Close() // クローズ時のエラーを無視 f.WriteString("Hello, world!") if err := f.Truncate(5); err != nil { log.Fatal(err) }
この結果、ファイル内容は
Hello
となります。
Sync メソッド
Sync メソッドはメモリ上のファイル内容をディスクに書き出して内容を同期します。今回は *os.File 型に定義されているメソッドを使って、ファイル情報を取得したりファイル内容の読み書きをしたりする方法を見てきました。 返り値やエラーなどをちょっと真面目に見たので逆にゴチャゴチャしてしまった感がありますが、普通に読み書きする分には使い方は簡単だと思います。
ただし、ファイル内容の一括読み込み、一括書き出しをする場合は、ioutil パッケージ(インポートパス "io/ioutil")に定義されているパッケージ関数を使った方が簡単です。 次回はこれらを見ていきます。
【修正】
- ファイルクローズ時のエラー処理を無視すると defer 文がもっと簡単に書けるので修正しました。
- *os.File が実装している io パッケージのインターフェースの箇所を追記しました。
- 何カ所かメソッドと書くべきところを関数と書いていたので修正しました。
- Seek メソッドのサンプルコードで io パッケージの定数を使うように修正しました。
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- 作者: Alan A.A. Donovan,Brian W. Kernighan,柴田芳樹
- 出版社/メーカー: 丸善出版
- 発売日: 2016/06/20
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (2件) を見る