はじめに

最近というほどでもないがfileを開いて編集したあと上書きをして保存するみたいなプログラムを実装した.
そのときにとあるバグを仕込んでしまったので書いておく.

ファイルを開くときについて

C言語とかを書いたことがある人ならわかると思うだろうが, fileを開いたときにはどういうmodeでファイルを開くかというmodeが存在する.
それがGo言語だろうがC言語だろうがOSがそういうふうにできていれば関係なく存在する.

例えばGo言語でfileをreadonlyとして開くときはしばしば os.Open 関数を利用する.

f, err := os.Open("適当なfile名")
if err != nil {
    // なんかエラー処理
}
...

os.Open の中身は次のようになっている.

func Open(name string) (*File, error) {
	return OpenFile(name, O_RDONLY, 0)
}

O_RDONLY がopen modeになっている.
ちなみに O_RDONLY 以外はこのようになっている.

mode type 意味
O_RDONLY 読み込み可
O_WRONLY 書き込み可
O_RDWR 読み書き可

詳しいいことは他の資料をあさって読んだほうがよく分かると思う.

Go言語でファイルを開くときは他にもいくつか方法がある.
例えば os.Create などだ.
os.Create の内部的には次のようになっている.

func Create(name string) (*File, error) {
  return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

os.Create は読み書き可能状態でファイルを開くが存在しないときはファイルを作成するし, 存在した場合は中に書かれている内容を消してから開く.

// 一回目実行すると `test.txt` が作成され中に `hoge` という文字列が記述される
package main

import "os"

func main() {
  f, err := os.Create("test.txt")
  if err != nil {
    panic(err)
  }
  f.Write([]byte("hoge"))
}
// test.txt がある状態で実行すると `O_TRUNC` が発動して中の文字列が消える
package main

func main() {
  os.Create("test.txt")
}

os.Open , os.Create に共通して言えることは, どちらも os.OpenFile を利用していることである.

問題が起きるときについて

自分でテキストエディタを実装したいときを考えてみる.
多分こんなかんじになる.

  1. os.Open でファイルを開く
  2. 編集する
  3. os.OpenFile で書き込み可能な状態で開く
  4. 書き込み可能な状態で開いたやつに書き込む

はじめ私はこんな具合で実装していた.

r, err := os.Open("適当なファイル名")
if err != nil {
  panic(err)
}
defer r.Close()

buffer := new(bytes.Buffer)
io.Copy(buffer, r)
// なんか編集する処理でbufferを編集

w, err := os.OpenFile(fileName, os.O_WRONLY, 0644)
if err != nil {
  panic(err)
}
defer w.Close()
io.Copy(w, buffer)

この処理はあまり良くない.
編集前の結果より編集後の結果が長くなるときは問題ないが, 編集前の結果より編集後の結果が長く為るとき問題となる.
どういうことか説明しよう.
前提条件として test.txt には hogehoge という文字列が書き込まれているものとする.

まず問題がないときのケースだ.

editedData := []byte("hugahugahuga")

w, err := os.OpenFile(fileName, os.O_WRONLY, 0644)
if err != nil {
  panic(err)
}

w.Write(editedData)

この場合は test.txt の中身は hugahugahuga になる.
問題ある場合だとこの様になる.

editedData := []byte("huga")

w, err := os.OpenFile(fileName, os.O_WRONLY, 0644)
if err != nil {
  panic(err)
}

w.Write(editedData)

test.txt の中身は hugahoge になってしまう.

問題の回避

このような自体を回避するためには, O_TRUNC を使う.
開いたときに前の文字列が消えるので古い文字列が残ることはない.