Golangで簡単に一時ファイルを吐くことを考える


# はじめに

インフラエンジニアな私の場合、Go言語で書くのは「daemonのように常時起動しているプログラム」より、「1回走って終わり」な簡単なプログラムであることが多い。このとき、前回の結果を参照できるかどうかで、できることがだいぶ違う。

前回の実行結果が取得できれば、アラートを何度も送ってしまうようなことはないし、いわゆるカウンタータイプのデータからCPU使用率が計算できたり、通信速度が計算できたりする。

だからといって、永続化のためにDBを使用するのはちょっとやり過ぎだと思う。これは[前回のログ出力の記事](http://qiita.com/Ets/items/49e8f781990a3b0b3821)も同じ思想だ。Pythonのshelveのように、（個人的に）簡単で使いやすい方法を考えたい。

# 書いてみた

出力方式は色々あるが、プレーンテキストにしてパーサを書くのは面倒である。
汎用性のあるJSON形式で出力することにした。


## サンプルコード全文

サンプルの動作のために、余計な部分が多い。

```sample.go
package main

import (
	"encoding/json"
	"os"
	"io/ioutil"
	// 以下はサンプル用のimport
	"fmt"
	"math/rand"
	"time"
)

type Data struct {
	Time   int64
	Value1 int64
	Value2 string
}

const DataFile = "/tmp/lastdata.json"

func writeDataMap(path string, dataMap *map[string]Data) error {
	if len(*dataMap) == 0 {
		// do nothing
		return nil
	}
	bytes, _ := json.Marshal(*dataMap)
	return ioutil.WriteFile(path, bytes, os.FileMode(0600))
}

func readDataMap(path string) (dataMap map[string]Data, err error) {
	file, err := ioutil.ReadFile(path)
	if err != nil {
		// do nothing
		return nil, err
	}
	err = json.Unmarshal(file, &dataMap)
	return
}

func main() {
	// 前回の値の取り込みと今回の値の準備
	lastDataMap, _ := readDataMap(DataFile)
	var newDataMap = map[string]Data{}


	// 現在のデータを集め、TestKeyという名前で登録する
	var newData Data
	newData.Time = time.Now().Unix()
	rand.Seed(newData.Time)
	value := rand.Int63n(1000)
	newData.Value1 = value
	newData.Value2 = fmt.Sprintf("Rawdata is %d", value)
	newDataMap["TestKey"] = newData

	// 差分計算
	if lastData, ok := lastDataMap["TestKey"]; !ok {
		// 前回の値がない場合は、計算をしない
		fmt.Println("This is firsttime.")
	} else {
		// 前回の値との差分を求める
		interval := newData.Time - lastData.Time
		differ := newData.Value1 - lastData.Value1

		fmt.Printf("last message = %s\n", lastData.Value2)
		fmt.Printf("current message = %s\n", newData.Value2)
		fmt.Printf("differ = %d & interval = %d\n", differ, interval)
		fmt.Printf("XXX per sec = %.2f\n", float64(differ)/float64(interval))
	}

	// 現在のデータを保存
	writeDataMap(DataFile, &newDataMap)
}

```

## 実行例

1回目の実行。

```bash
$ /vagrant/src/test/test_stat
This is firsttime.

$ ls -l /tmp/lastdata.json
-rw------- 1 user user 70  7月 23 19:17 2015 /tmp/lastdata.json

$ cat /tmp/lastdata.json
{"TestKey":{"Time":1437646657,"Value1":356,"Value2":"Rawdata is 356"}}
```

よし、JSONファイルが作られている。

もう1回実行する。

```bash
$ /vagrant/src/test/test_stat
last message = Rawdata is 356
current message = Rawdata is 990
differ = 634 & interval = 15
XXX per sec = 42.27
```

前回の値を参照して差分計算をした。

## コードの説明

サンプル実装のせいで見た目は結構長いが、核となる部分は簡単だ。
重要なのは以下。

* Data構造体
* `var newDataMap = map[string]Data{}` という宣言
* * writeDataMap関数
* readDataMap関数

### Data構造体

必要な情報を格納する構造体を予め宣言しておく。
これは、保存したデータを取り込むときに、型変換などを気にせずに扱えるようにするためだ。

[ここ](http://qiita.com/oahiroaki/items/ff21d9adfb843161d1d8)に書かれているように、任意のJSONヘッダにすることもできる。将来的な互換性のためにはそうすべきだろう。


### `var newDataMap = map[string]Data{}` という宣言

このサンプルコードでは、先ほどのData構造体を、map(dataMap変数)に突っ込んでいる。これはこのサンプルコードでの取り扱いなので、必ずしも従う必要はない。

ただ、こうすることで **本来保存したいデータ** と **メタデータ** を保存・読込できたりと、便利になる。というか、1ファイルに1データしか保存しないのも勿体無い感じがするし。

mapをJSON化する際は、 **Keyは必ずstringでなければならない** ことに注意。


### writeDataMap関数

このサンプルでは、この2行が全て。

```go
bytes, _ := json.Marshal(*dataMap)
return ioutil.WriteFile(path, bytes, os.FileMode(0600))
```

Data構造体のmapを、`json.Marshal`関数でバイト列に変換して出力する。
前のファイルは上書きされる。クローズ処理もいらない。

かなり雑なので、本来はエラー処理などもちゃんと書くべきと思う・・・


### readDataMap関数

readもwriteとほぼ一緒で、重要なのはほんの数行。

```go
(関数定義の)　dataMap map[string]Data

file, err := ioutil.ReadFile(path)
err = json.Unmarshal(file, &dataMap)
```

writeした時と同じ構造のデータ型に`json.Unmarshal`すればいい。




# 終わりに

色々気にはなるが、Pythonよりも厳しいGo言語でこのくらいのコード量で済むなら許容範囲かと。ただ、JSONというのが最大の難点でもある。

simplejson使うべきかなぁ。
参考: http://astaxie.gitbooks.io/build-web-application-with-golang/content/ja/07.2.html