はじめに
今回はGo言語でのファイルの読み書きの方法を今回はまとめてみました。
ファイルをオープンして書き込む
まず、一般的なファイルを開いて書き込む方法についてです。
以下は、読み書きを2種類の方法で行っています。data1とdata2というファイル名で中にHello World!と書き込むコードです。
蛇足ですが、Go1.16以降ではioutilパッケージのいくつかの関数が推奨されなくなり、osパッケージに移動されています。今回のWriteFileとReadFileも元々はioutilの関数でしたが使おうとすると警告が出ます。
package main
import (
"fmt"
"os"
)
func main() {
data := []byte("Hello World!\n")
err := os.WriteFile("data1", data, 0644)
if err != nil {
panic(err)
}
read1, _ := os.ReadFile("data1")
fmt.Print(string(read1))
// os.WriteFile()もあるけど何だこれ
file1, _ := os.Create("data2")
defer file1.Close()
bytes, _ := file1.Write(data)
fmt.Printf("Wrote %d bytes to file\n", bytes)
file2, _ := os.Open("data2")
defer file2.Close()
read2 := make([]byte, len(data))
bytes, _ = file2.Read(read2)
fmt.Printf("Read %d bytes from file\n", bytes)
fmt.Println(string(read2))
}
コードが長いため重要な部分を抜粋して確認します。
os.WriteFile
この関数は、指定したファイルにデータの内容を書き込みます。ファイルが存在しない場合は新しいファイルが作成され、既存のファイルがある場合はその内容が上書きされます。
簡潔に説明すると、ファイルが存在しない場合、"data1"という指定されたファイルにデータを書き込み、適切な権限を設定しています。
err := os.WriteFile("data1", data, 0644)
WriteFile
の定義は次の通りです。エラーがある場合のみ返り値があります。
func WriteFile(name string, data []byte, perm FileMode) error {
f, err := OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
そして、引数で指定する値は以下の通りです。
- 第一引数:ファイル名や相対パス等で指定したファイル
- 第二引数:書き込むデータ(バイトスライス)
- 第三引数:ファイルのパーミッション
今回は0644
と権限を与えています。つまり、所有者は読み書きができ、グループと他のユーザーは読み取り専用であることを意味します。
パーミッションの補足
パーミッションの形式3桁または4桁で表されます。
3桁形式
最初の桁(ユーザー): 所有者の権限
2番目の桁(グループ): グループの権限
3番目の桁(他のユーザー): その他のユーザーの権限
4桁形式
最初の桁(特別なビット): setuid, setgid, stickyビット
2番目の桁(ユーザー): 所有者の権限
3番目の桁(グループ): グループの権限
4番目の桁(他のユーザー): その他のユーザーの権限
パーミッションの値
読み取り(r): 4
書き込み(w): 2
実行(x): 1
os.ReadFile
続いて、WriteFile
で作成したdata1
というファイルを以下の箇所で読み込みます。
read1, _ := os.ReadFile("data1")
メソッドの定義は長いので抜粋しますが、簡潔に説明すると、
func ReadFile(name string) ([]byte, error){
...
, err := Open(name)
...
data := make([]byte, 0, size)
for {
n, err := f.Read(data[len(data):cap(data)])
data = data[:len(data)+n]
if err != nil {
if err == io.EOF {
err = nil
}
return data, err
}
...
ファイルを開いた後に、開いたファイルからデータを読み込み、そのデータをメモリ上のバッファに格納します。そして、読み込んだデータをバイトスライスで返却します。ファイルの読み込み中にエラーが発生した場合はそのエラーも返します。
ここまでのまとめ
os.WriteFile(filename, data, permission)
でファイルにデータを書き込み、os.ReadFile(filename)
でファイルのバイトスライスを取得できる
os.Create
続いて、os.Create
です。os.Create
はos.File構造体のポインタ
を返します。
この関数は、指定された名前の新しいファイルを作成します。もし同じ名前のファイルが既に存在する場合、そのファイルは上書きされます。
os.WriteFile
と似ていますが、os.WriteFile
は書き込みまで行ってくれる一方で、os.Create
はファイルの作成をするだけで。別途、書き込み、クローズを分けて行う必要があるため、より細かい制御が可能です。
file1, _ := os.Create("data2")
定義は以下の通りです。
OpenFile
では権限が0666
で全ユーザーに読み書き権限が与えられています(さすがに設定できると思うのですが、探しきれませんでした。)
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
興味深いのがos.ReadFile
とos.Create
ともにOpen関数
を使用していますが、os.ReadFile
は開いたos.File構造体のポインタ
を処理してバイトスライスを返却する一方で、os.Create
は処理する前のos.File構造体のポインタ
を返すという点です。
*File.Write
そして、上述のos.Create
で返却されたFile構造体に紐づいたWrite
メソッドを使用してデータを書き込みます。
bytes, _ := file1.Write(data)
Writeメソッド
の定義は以下の通りです。
この関数ではデータをバイトスライスで受け取り、f.write
で書き込み、書き込んだバイト数と発生したエラーを返却します。
func (f *File) Write(b []byte) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
n, e := f.write(b)
if n < 0 {
n = 0
}
if n != len(b) {
err = io.ErrShortWrite
}
epipecheck(f, e)
if e != nil {
err = f.wrapErr("write", e)
}
return n, err
}
os.Open
次にos.Open
ですが、これは上述のos.Create
に非常によく似ていますが、名前の通り、ファイルを作成することはなく、既存のファイルを読み取り専用で開きます。
ただし、返り値はos.File構造体のポインタとエラーと同じです。 os.Create
との違いは、フラグとパーミッションです。
定義は以下の通りです。
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
このフラグはOpenFile
関数の以下の部分で使用されており、フラグによってappendMode
がtrue/falseに切り替わるようになっています。
- trueの場合はファイルにデータを書き込むとき、常にファイルの末尾に追加されます。
- falseの場合はファイルにデータを書き込むとき、現在のファイルポインタの位置に書き込みが行われます。ファイルポインタがファイルの先頭にある場合、既存のデータが上書きされます。
f.appendMode = flag&O_APPEND != 0
os.Open
ではフラグがO_RDONLY
で0x0
、O_APPENDは0x8
と定義されているので、f.appendMode = flag&O_APPEND != 0
は0000
と1000
が0
となるため、falseとなります。
ビットごとのAND演算は、両方のビットが1である場合にのみ1を返し、それ以外の場合、そのビットを0に設定します。
*File.Read
最後にos.Open
で返却されたファイルを以下の箇所で読み込みます。f.Read
は指定されたバイストスライスに読み取ったデータを格納します。
つまり、以下は、ファイルfile2からデータを読み取り、バッファread2に格納するためのコードです。
bytes, _ = file2.Read(read2)
こちらも*Fileのメソッドで、以下の通り定義されています。
func (f *File) Read(b []byte) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
return n, f.wrapErr("read", e)
}
実はこのメソッドはos.ReadFile
の以下の箇所でも使用されています。
n, err := f.Read(data[len(data):cap(data)])
f.Read
は指定されたバイスとスライスに読み取ったデータを格納するので、ReadFile
の中ではf.Read
を使用し、開いたファイルをバイト数を確認しながらデータをバイトスライスに格納し、最終的にそのバイトスライスを返却しています。
一方で、f.Read
だけでは、ただ指定されたバイトスライスにデータを格納するだけです。
つまり、厳密には違いますが、めちゃくちゃざっくり言うと、os.Openして、os.Readする作業はos.ReadFileで完結する。 ってことですかね。
まとめ
細かく説明してしまったので長くなりましたが、最後に表でまとめて一目で分かるようにしておきました。以下に、各関数とメソッドの特徴をまとめた表を作成しました。
機能 | 説明 | 使用例 | 返り値 |
---|---|---|---|
os.WriteFile |
指定したファイルにデータを書き込む。ファイルが存在しない場合は新規作成、存在する場合は上書き | os.WriteFile("file_name", []byte, 0644) |
error |
os.ReadFile |
指定したファイルからデータを読み取る。ファイルの内容をバイトスライスとして返す | read, _ := os.ReadFile("file_name") |
[]byte, error |
os.Create |
指定した名前の新しいファイルを作成する。既存のファイルがある場合は上書き。ファイルハンドルを返す | file, _ := os.Create("file_name") |
*os.File, error |
os.Open |
指定した名前の既存のファイルを読み取り専用で開く。ファイルハンドルを返す | file, _ := os.Open("file_name") |
*os.File, error |
*File.Write |
開かれたファイルにデータを書き込む。書き込んだバイト数とエラーを返す | bytes, _ := file.Write([]byte) |
int, error |
*File.Read |
開かれたファイルからデータを読み取る。読み取ったデータを指定されたバイトスライスに格納 | bytes, _ = file.Read([]byte) |
int, error |
説明の中で*os.File
と何度か説明していましたが、それがどのようなものなのか記述していなかったので追記です。
type File struct {
*file // os specific
}
...
type file struct {
pfd poll.FD
name string
dirinfo *dirInfo // nil unless directory being read
nonblock bool // whether we set nonblocking mode
stdoutOrErr bool // whether this is stdout or stderr
appendMode bool // whether file is opened for appending
}