ファイル全体を読み書きする
ほとんどの用途は os.WriteFile と os.ReadFile でまかなえます。開く、書くか読む、閉じるを1回の呼び出しで行います。
このページの例は、os.MkdirTemp で作った一時ディレクトリで作業し、defer os.RemoveAll(dir) で削除するので、何も残しません。自分のコードでは、"config.json" のような相対パスはプロセスの作業ディレクトリを基準に解決されます。それはソースファイルやバイナリのディレクトリとは限りません。
os.WriteFile は必要ならファイルを作り、存在すれば 切り詰めます。3つ目の引数は、新しく作るファイルのUnixのパーミッションです。0o644 は所有者が読み書きでき、他の人は読めるという意味です。すでにあるファイルでは無視され、プロセスのumaskによってビットが削られることもあります。
os.ReadFile はすべてをメモリに読み込みます。設定ファイルや小さな入力にはそれが正しく、数ギガバイトのログには間違いです。
bufio.Scannerで1行ずつ読む
大きなファイルや、どのみち行単位で扱いたいときは bufio.Scanner を使います。まとまった単位で読み、改行を除いた行を1つずつ渡してくれます。
細かな点が3つあります。
- ループの後で
sc.Err()をチェックする。Scanはファイルの終わりでもエラーでもfalseを返し、区別できるのはErrだけです。 - 64KBの行の上限。 デフォルトでは、64KBより長い1行があるとスキャナーは
bufio.Scanner: token too longで止まります。長い行を含むファイル(圧縮されたJSON、一部のログ)では、ループの前に上限を引き上げます:sc.Buffer(make([]byte, 1024*1024), 10*1024*1024)。 - 他の単位。
sc.Split(bufio.ScanWords)は単語を、bufio.ScanRunesは文字を返します。
行ではなく固定サイズのかたまりでストリームを読むには、ループで f.Read(buf) を使うか、io.Copy で別のWriterに渡します。
書き込み:os.Create、os.OpenFile、追記
os.Create(name) は書き込み用にファイルを開き、作成するか切り詰めます。os.OpenFile はフラグですべてを制御できます。
| フラグ | 意味 |
|---|---|
os.O_RDONLY、os.O_WRONLY、os.O_RDWR | 読み取り、書き込み、または両方のために開く(1つを選ぶ) |
os.O_CREATE | ファイルがなければ作る |
os.O_TRUNC | 開くときにファイルを空にする |
os.O_APPEND | すべての書き込みが末尾に行われる |
os.O_EXCL | O_CREATE と組み合わせて、ファイルがすでにあれば失敗する |
os.Open(name) は OpenFile(name, O_RDONLY, 0) と同じです。os.Create(name) は OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0o666) と同じです。
書き込むときは Close のエラーが重要です。ファイルシステムによっては、書き込みの失敗をクローズの時点でしか報告しないので、defer f.Close() だけではデータの喪失を隠してしまうことがあります。書き込むファイルでは、appendLine のように Close を明示的にチェックします。読むだけのファイルなら defer f.Close() で問題ありません。
fmt.Fprintln をはじめとするfmtの表示関数は、ファイルを含む任意の io.Writer を受け付けます。bufio.Writer は小さな書き込みをメモリにためてまとめます。w.Flush() を忘れるのは典型的なバグで、プログラムは正常に終了し、最後の数キロバイトがファイルに届きません。
ファイルは存在するか
Goに os.Exists はありません。os.Stat を呼んでエラーを調べます。
errors.Is(err, fs.ErrNotExist) が現在のイディオムです。ラップされたエラーを見通せない古い os.IsNotExist(err) に代わるものです。
開く前にチェックするのは、たいてい不要で、競合も起こします。チェックと開く操作の間にファイルが現れたり消えたりしうるからです。普通は単に開いて、開く操作から返る fs.ErrNotExist を処理します。まだ存在しないときだけファイルを作るには、O_CREATE|O_EXCL を使うと、チェックと作成が1つの不可分な手順になります。
ディレクトリ
| やりたいこと | 関数 |
|---|---|
| ディレクトリを1つ作る | os.Mkdir(path, 0o755) |
| 親を含めてパスを作る | os.MkdirAll(path, 0o755) |
| ディレクトリの一覧 | os.ReadDir(path) |
| ツリーをたどる | filepath.WalkDir(root, fn) |
| ファイルか空のディレクトリを削除する | os.Remove(path) |
| ツリーを削除する | os.RemoveAll(path) |
| 名前の変更や移動 | os.Rename(old, new) |
| 一時ファイルや一時ディレクトリ | os.CreateTemp("", "prefix-*")、os.MkdirTemp("", "prefix") |
| パスの部品をつなぐ | filepath.Join(a, b, c) |
ファイルシステムのパスには path/filepath を使います。OSに合った区切り文字(Windowsでは \)を使ってくれます。path パッケージは、URLのようなスラッシュ区切りのパスのためのものです。
Go 1.24では os.Root(os.OpenRoot(dir))も追加されました。1つのディレクトリの中でだけファイルを開き、.. やシンボリックリンクで外に出るパスを拒否します。ファイル名がユーザーから来る場合に使います。
よくある間違い
- エラーをチェックしない。 これらの呼び出しはどれも失敗しえます。開けなかったファイルは
nilで、その後のRead、Write、Closeはすべてinvalid argumentを返し、本当の原因(ファイルがなかった、権限がなかった)を隠します。 - スキャンのループの後で
sc.Err()を忘れる。 読み取りのエラーがファイルの終わりに見えます。 bufio.WriterのFlushを忘れる。 ファイルの終わりが欠けます。- 書き込み後の
Closeのエラーを無視する。 書き込みのエラーはそこでしか表に出ないことがあります。 - 多くのファイルをループする中での
defer f.Close()。 関数が戻るまでファイルが開いたままになり、ファイルディスクリプタを使い果たすことがあります。本体を関数に移して、反復ごとにファイルが閉じるようにします。 - パーミッションを10進数で書く。
644は0o644ではありません。Goは644を10進数として読むので0o1204になり、おかしなビットが立ちます。
よくある質問
Goでファイル全体を文字列として読むには?
data, err := os.ReadFile("notes.txt") は内容を []byte で返すので、string(data) で変換します。ファイルを開いて読んで閉じるところまでやってくれます。メモリに余裕で収まるファイルに使い、大きなファイルは bufio.Scanner で1行ずつ読みます。
Goでファイルを1行ずつ読むには?
os.Open でファイルを開き、defer f.Close() とし、bufio.NewScanner(f) で包んで、for sc.Scan() { line := sc.Text() } でループし、ループの後で sc.Err() をチェックします。64KBより長い行があると、sc.Buffer でバッファを大きくしない限り、スキャナーは token too long で失敗します。
Goでファイルに追記するには?
os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) で開き、書き込んで、Close のエラーをチェックします。O_CREATE はファイルがなければ作り、O_APPEND はすべての書き込みを末尾に行わせます。
Goでファイルが存在するか確認するには?
os.Stat(path) を呼び、そのエラーを errors.Is(err, fs.ErrNotExist) でテストします。エラーがnilなら存在します。それ以外のエラー(たとえばpermission denied)は判断がつかないことを意味するので、「存在しない」として扱わず、別に処理します。