はじめに
インフラエンジニアな私の場合、Go言語で書くのは「daemonのように常時起動しているプログラム」より、「1回走って終わり」な簡単なプログラムであることが多い。このとき、前回の結果を参照できるかどうかで、できることがだいぶ違う。
前回の実行結果が取得できれば、アラートを何度も送ってしまうようなことはないし、いわゆるカウンタータイプのデータからCPU使用率が計算できたり、通信速度が計算できたりする。
だからといって、永続化のためにDBを使用するのはちょっとやり過ぎだと思う。これは前回のログ出力の記事も同じ思想だ。Pythonのshelveのように、(個人的に)簡単で使いやすい方法を考えたい。
書いてみた
出力方式は色々あるが、プレーンテキストにしてパーサを書くのは面倒である。
汎用性のあるJSON形式で出力することにした。
サンプルコード全文
サンプルの動作のために、余計な部分が多い。
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回目の実行。
$ /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回実行する。
$ /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構造体
必要な情報を格納する構造体を予め宣言しておく。
これは、保存したデータを取り込むときに、型変換などを気にせずに扱えるようにするためだ。
ここに書かれているように、任意のJSONヘッダにすることもできる。将来的な互換性のためにはそうすべきだろう。
var newDataMap = map[string]Data{}
という宣言
このサンプルコードでは、先ほどのData構造体を、map(dataMap変数)に突っ込んでいる。これはこのサンプルコードでの取り扱いなので、必ずしも従う必要はない。
ただ、こうすることで 本来保存したいデータ と メタデータ を保存・読込できたりと、便利になる。というか、1ファイルに1データしか保存しないのも勿体無い感じがするし。
mapをJSON化する際は、 Keyは必ずstringでなければならない ことに注意。
writeDataMap関数
このサンプルでは、この2行が全て。
bytes, _ := json.Marshal(*dataMap)
return ioutil.WriteFile(path, bytes, os.FileMode(0600))
Data構造体のmapを、json.Marshal
関数でバイト列に変換して出力する。
前のファイルは上書きされる。クローズ処理もいらない。
かなり雑なので、本来はエラー処理などもちゃんと書くべきと思う・・・
readDataMap関数
readもwriteとほぼ一緒で、重要なのはほんの数行。
(関数定義の) 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