倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Go 言語の os パッケージにある File 型を使ってみる (2) : os.File のメソッド

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)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

*1:Chown でユーザ ID や グループ ID を使う場合には、user パッケージ(インポートパスは "os/user")を使います。 ただし、user パッケージに定義されている型 User, Group では Windows 上でも動くようにするためか、ユーザ ID や グループ ID は文字列として表現されているので int 値への変換が必要です。

*2:ファイルディスクリプタ, FD:ファイルへの参照を抽象化したキー(Wikipedia より)。