倭マン's BLOG

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

Go 言語の os パッケージにある File 型を使ってみる (1) : os.File オブジェクトを取得する関数

Go 言語のいろいろなパッケージを使ってみるシリーズ(目次)。 今回は os パッケージの os.File オブジェクトを取得するパッケージ関数を見ていきます。

【この記事の内容】

os.File オブジェクトを取得するパッケージ関数

os パッケージのパッケージドキュメントはこちら。 os.File オブジェクトを取得する関数には以下のようなものがあります:

type File
  func Open(name string) (*File, error)    // 読み込み
  func Create(name string) (*File, error)  // 書き出し
  func OpenFile(name string, flag int, perm FileMode) (*File, error)
  func NewFile(fd uintptr, name string) *File
  func Pipe() (r *File, w *File, err error)

os.File 型に関連して、ファイルモードを表す os.FileMode 型(uint32 のビットフラグ)も見ていきます。

os.Open 関数

既に存在しているファイルから内容を読み取りたい場合には、os.Open 関数を使います:

  // ファイルを開く
  f, err := os.Open("hello-world.txt")
  if err != nil {
    log.Fatal(err)
  }

  content, _ := ioutil.ReadAll(f)  // ファイル内容の一括読み込み(エラー無視)
  fmt.Println(string(content))

  // ファイルを閉じる
  if err := f.Close(); err != nil {
    log.Fatal(err)
  }

os.Open 関数でファイルを開いた場合、後述するファイルを開くフラグで os.O_READONLY が指定されたのと同じです。 つまり書き込みはできません。

os.Create 関数

ファイルへ何かを書き出したい場合は、os.Create 関数を使います。 os.Create 関数は指定したファイルが存在してもエラーを返さず作成し、元のファイルの内容は削除されるので注意。

  // ファイルを作成する
  f, err := os.Create("hello-world.txt")
  if err != nil {
    log.Fatal(err)
  }

  f.WriteString("Hello, world!")  // ファイルへの書き出し

  // ファイルを閉じる
  if err := f.Close(); err != nil {
    log.Fatal(err)
  }

ファイルモード(POSIX パーミッション)は 0666 (rw-rw-rw-) で作成され、読み書きが可能です。 後述のファイルを開くフラグで os.O_RDWR を指定したのと同じです。

os.OpenFile 関数

ファイルの簡単な読み書きをするには os.Open / os.Create 関数で充分ですが、もっと細かな設定とともにファイルを開きたい場合は os.OpenFile 関数を使います。 os.OpenFile 関数は以下の引数を取ります:

  1. ファイル名 string
  2. フラグ int
  3. ファイルモード(POSIX パーミッション) FileMode

ファイル名はいいとして、その他の引数を見ていきましょう。

ファイルを開くときに指定するビットフラグ
第2引数のフラグは os パッケージに定義されているビットフラグ定数を指定します:

// ファイルを開くときに指定するビットフラグ定数
const (
  O_RDONLY int  // 読み込み専用
  O_WRONLY int  // 書き込み専用
  O_RDWR   int  // 読み書き可能
  O_CREATE int  // ファイルが無ければ新規作成
  O_TRUNC  int  // ファイル内容を削除
  O_APPEND int  // ファイル後端に追加で書き込み
  O_EXCL   int  // (O_CREATE とともに用いて)ファイルが存在すればエラー
  O_SYNC   int  // 入出力が同期されたファイル
)

例えば、ファイルを読み書き可能、無ければ作成、既に存在するならエラーを返す、という条件で開きたい場合、

  f, err := os.OpenFile("hello-world.txt", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0755)
  if err != nil {
    log.Fatal(err)
  }

  ...

とします。

ファイルモード FileMode
第3引数のファイルモードもビットフラグ定数(ただし uint32)です:

type FileMode uint32

// ファイルモードの定数
const (
  ModeDir        FileMode = 1 << (32 - 1 - iota) // d
  ModeAppend                                     // a
  ModeExclusive                                  // l
  ModeTemporary                                  // T
  ModeSymlink                                    // L
  ModeDevice                                     // D
  ModeNamedPipe                                  // p
  ModeSocket                                     // S
  ModeSetuid                                     // u
  ModeSetgid                                     // g
  ModeCharDevice                                 // c
  ModeSticky                                     // t

  ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice
  ModePerm FileMode = 0777  // ファイルの POSIX パーミッションのためのフィルタ
)

上記の定数で ModePerm 以外は32ビットの上位ビットいくつかのフラグで、モードがディレクトリやシンボリックリンクなどのファイルタイプなどを含むものが定義されています(他にもありますが)*1

通常のファイルの読み書きをしたい場合はこれらを気にする必要はなく*2、32ビットの下位9ビットで POSIX パーミッションを指定します。 POSIX パーミッションに対しては定数が定義されておらず、(このためだけに表記が導入されたらしい)0から始まる8進の整数リテラルを指定します。 たとえばパーミッションとして「rwxr-xr-x」を与えたい場合は「0755」を指定します。 「rw-rw-rw-」なら「0666」です(os.Create 関数の場合):

  f, err := os.OpenFile("hello-world.txt", os.O_RDWR|os.O_CREATE, 0755)
  // POSIX パーミッション「rwxr-xr-x」(0755) でファイルを作成
  if err != nil {
    log.Fatal(err)
  }
  
  ...

os.NewFile 関数

os.NewFile 関数は uintptr 型のファイル記述子とファイル名を指定してファイルを作成します。 が、普通にプログラミングしていて、どの程度使うものなのか不明。 ファイル記述子が不正なら、エラーを返さず(そもそも返り値にエラーが宣言されていない)nil を返します。

標準入出力 os.Stdin/os.Stdout や標準エラー出力 os.Stderr は os パッケージの変数として宣言されていますが、NewFile 関数で初期化されているようです:

var (
  Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
  Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
  Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

os.Pipe 関数

os.Pipe はパイプされた io.Reader と io.Writer の組を生成します。 つまり、返された io.Writer に書き込んだ内容をパイプされた io.Reader から読み込むことができます。 使い方は以下のようになります:

  // パイプされた io.Reader/io.Writer の組を生成
  r, w, err := os.Pipe()
  if err != nil { log.Fatal(err) }

  go func(){
    for i := 0; i < 10; i++ {
      // io.Writer へ書き出し
      w.WriteString(fmt.Sprintf("Hello, world%d!\n", i))
      time.Sleep(100*time.Millisecond)
    }
    if err := w.Close(); err != nil { log.Fatal(err) }
  }()

  bs := make([]byte, 20)
  for {
    time.Sleep(1*time.Second)

    // io.Reader から読み込み
    n, err := r.Read(bs)
    if err == io.EOF {
      break
    } else if err != nil {
      log.Panic(err)
    }

    fmt.Print(string(bs[:n]))
  }

  if err := r.Close(); err != nil { log.Fatal(err) }

io.Writer に何も書き出されていなくても io.Reader から読み込みはできますが、当然内容はありません(読み込みをブロックしたりはしないもよう)。

これで os.File オブジェクトを取得する関数は一通り見ました。 当初、os.File に定義されているメソッドも見ていく予定でしたが、長くなりそうなので次回に回します。

【修正】

  • os.Pipe 関数のサンプルコードで、読み込み部分を少し修正しました(io.Pipe 関数の場合でも動くようにしました)。

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

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

*1:上記の定数の定義でコメントとして載せてある文字(ModeDir の d など)は、ファイルモードの文字列表記がされた場合に表示される略記です。 たとえば、ディレクトリに対して FileMode オブジェクトを取得して、文字列として表示させると「drwxrwxrwx」のように先頭に「d」が表示されます。 あまり詳しくないですが、Windows でも PowerShell で同じような表示がされるかと思います。

*2:ディレクトリを作成したい場合は os.Mkdir, os.MkdirAll 関数を使うなど、他の便利な方法があるものもあります。