【JSON】Go の Tips

Go の Tips をいくつか書き残しておきます。

  • json.Marshalで “<“, “>”, “&” をエスケープさせたくない
  • JSON を簡単に扱いたい
  • 日時文字列を TimeStamp に変換したい
  • ファイルの存在確認をしたい
  • 見通しの良いユニットテストを書きたい
  • HTTP Server の Handler のテストを書きたい
  • ハッシュ文字列を数値列に変換したい
  • Auto Build Versioning したい
  • map で function を使いたい

環境は go 1.7.4 (darwin/amd64) です。

json.Marshalで “<“, “>”, “&” をエスケープさせたくない

API Server を書いている時に, 以下のような JavaScript コードの文字列を []byte に変換し返したい。

javaScriptString := "<script type=text/javascript\">console.log(\"Hello World!!\")</script>"

json.Marshal だと “<“, “>”, “&” は強制的にエスケープされる。

"\u003cscript type=text/javascript\"\u003econsole.log(\"Hello World!!\")\u003c/script\u003e"

`bytes.Replace` でエスケープされた部分を書き換えるようにする。

func JSONSafeMarshal(v interface{}, safeEncoding bool) ([]byte, error) {
	b, err := json.Marshal(v)
	if safeEncoding {
		b = bytes.Replace(b, []byte("\\u003c"), []byte("<"), -1)
		b = bytes.Replace(b, []byte("\\u003e"), []byte(">"), -1)
		b = bytes.Replace(b, []byte("\\u0026"), []byte("&"), -1)
	}
	return b, err
}

意図した形になるので JavaScript側で eval() が成功する。

JSON を簡単に扱いたい

Go で JSON を扱うのはやや不便なところがある。 対応する構造体を書く場合が多いが, 何重にもネストしていたりすると大変な場合がある。

{
	"status": true,
	"version": 1.0,
	"item": {
		"id": 201023
	},
	"months": [{
		"1": {"count": 7},
		"2": {"count": 3},
		"3": {"count": 5}
	}]
}

`github.com/bitly/go-simplejson` を使うと JSON の構造が予めわかっていない時でも LL の感覚で扱えて便利。

package main

import (
	"fmt"
	"io/ioutil"
	"github.com/bitly/go-simplejson"
)

func main() {
	f, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		fmt.Println(err)
	}

	json, err := simplejson.NewJson(f)
	if err != nil {
		fmt.Println(err)
	}

	// Float
	fmt.Println(json.Get("version").Float64())
	// Bool
	fmt.Println(json.Get("status").Bool())
	// Array
	ary, _ := json.Get("months").Array()
	fmt.Println(ary[0])
	// Int
	fmt.Println(json.GetPath("item", "id").Int64())
}

日時文字列を TimeStamp に変換したい

時間関係は time パッケージで何とかなる印象がある。

package main

import (
	"fmt"
	"time"
)

func main() {
	target := "2017-01-26T10:00:00"
	loc, err := time.LoadLocation("Asia/Tokyo")
	t, err := time.ParseInLocation("2006-01-02T15:04:05", target, loc)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(t.Unix())
}

ParseInLocation() の layout に指定している “2006-01-02T15:04:05” は Go での時間フォーマットである。
1月2日 3時4分5秒 2006年 = 123456 という並びのためらしいが独特なのでわかりにくい感じはする。

$ go run datetime_to_timestamp.go
1485392400

ファイルの存在確認をしたい

設定ファイルが必要なプログラムを書いているときなど, ファイルの存在確認をしたい場合がある。

package main

import (
	"fmt"
	"os"
	"os/user"
	"path/filepath"
	"strings"
)

func main() {
	u, _ := user.Current()

	ss := []string{"foo.go", ".bashrc", "bar.go", ".vimrc"}

	for _, v := range ss {
		splits := strings.Split(v, "/")
		s := append([]string{u.HomeDir}, splits...)
		fname := filepath.Join(s...)
		_, err := os.Stat(fname)
		if !os.IsNotExist(err) {
			fmt.Println(fname, "is exist.")
		} else {
			fmt.Println(fname, "is not exist.")
		}
	}
}
$ go run file_check.go
/Users/xxx/foo.go is not exist.
/Users/xxx/.bashrc is exist.
/Users/xxx/bar.go is not exist.
/Users/xxx/.vimrc is exist.

見通しの良いユニットテストを書きたい

TableDrivenTests というのが良さそう。
例として渡された数値を2乘する関数をテストする。

package square

func Square(x int) int {
	return x*x
}

input と expected のペアを宣言し for で回すことでテストコードがすっきりして良い。

package square

import (
	"testing"
)

func TestSuqare(t *testing.T) {
	candidates := []struct {
		input    int
		expected int
	}{
		{2, 4},
		{5, 25},
		{8, 64},
	}

	for _, c := range candidates {
        result := Square(c.input)
		if result != c.expected {
			t.Errorf("expected %v, but %v", c.expected, result)
		}
	}
}

また, cover オプションでカバレッジ(C0)を測定できる。

$ go test --cover square.go square_test.go
ok      command-line-arguments  0.011s  coverage: 100.0% of statements

HTTP Server の Handler のテストを書きたい

以下のような HTTP Server の Handler のテストを行いたい。

package main

import (
	"fmt"
	"github.com/julienschmidt/httprouter"
	"html"
	"log"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprintf(w, "Hello %q", html.EscapeString(r.URL.Path))
}

func main() {
	router := httprouter.New()
	router.GET("/:path", helloHandler)

	log.Fatal(http.ListenAndServe(":8080", router))
}

“net/http/httptest” を使う。 `httptest.NewServer(router)` のように router や mux を渡すと内部でサーバが立ち上がる。
http.Get() で GETリクエストを投げてレスポンスのテストが行える。

package main

import (
	"github.com/julienschmidt/httprouter"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"testing"
)

func parse(resp *http.Response) (string, int) {
	defer resp.Body.Close()
	contents, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	return string(contents), resp.StatusCode
}

func TestHelloHandler(t *testing.T) {
	router := httprouter.New()
	router.GET("/:path", helloHandler)

	ts := httptest.NewServer(router)
	defer ts.Close()

	candidates := []struct {
		input    string
		expected string
	}{
		{"/world", `Hello "/world"`},
		{"/hoge", `Hello "/hoge"`},
	}

	for _, c := range candidates {
		r, err := http.Get(ts.URL + c.input)
		if err != nil {
			t.Errorf("expected %v, but %v", nil, err)
		}
		resp, status := parse(r)
		if status != http.StatusOK {
			t.Errorf("expected %v, but %v", 200, status)
		}
		if resp != c.expected {
			t.Errorf("expected %v, but %v", c.expected, resp)
		}
	}
}

ハッシュ文字列を数値列に変換したい

例として, MD5で得られた16進数ハッシュ文字列を “math/big” パッケージで数値列に変換する。

package md5

import (
	"crypto/md5"
	"encoding/hex"
	"math/big"
)

func Encode(s string) string {
	hash := md5.New()
	hash.Write([]byte(s))
	hexstr := hex.EncodeToString(hash.Sum(nil))
	return hexstr
}

func ToNumber(hexstr string) string {
	b := big.NewInt(0)
	b.SetString(hexstr, 16)
	return b.String()
}

Auto Build Versioning したい

Go で Auto Build Versioning したい時は `-ldflags` を活用することで簡単にできる。
例として ビルド日時, Git の commit hash, Go のバージョンを埋め込んでみる。

package main

import (
	"fmt"
)

var (
	buildDate string
	gitHash   string
	goVersion string
)

func main() {
	fmt.Println("Build date:", buildDate, "Git Hash:", gitHash, "Go:", goVersion)
}

結果は以下。

$ go build -ldflags "-X \"main.buildDate=`date '+%Y-%m-%d %I:%M:%S'`\" -X \"main.gitHash=`git rev-parse HEAD`\" -X \"main.goVersion=`go version`\"" -o main version.go
$ ./main
Build date: 2017-02-19 05:02:46 Git Hash: 1d67a0bf7f672b0edccf0c98dc6e48274fcd2f88 Go: go version go1.7.4 darwin/amd64

map で function を使いたい

map の value に function を書くことができる。以下は関数 f に基本的な算術演算子を渡すと map の key に一致した value である lambda 式の結果を返す例。

package main

import "fmt"

var operators = map[string]func(int, int) int{
	"+": func(a, b int) int { return a + b },
	"-": func(a, b int) int { return a - b },
	"*": func(a, b int) int { return a * b },
	"/": func(a, b int) int { return a / b },
}

func f(op string, a, b int) int {
	for i, _ := range operators {
		if op == i {
			return operators[op](a, b)
		}
	}
	return 0
}

func main() {
	for _, op := range []string{"+", "-", "*", "/", "¥"} {
		fmt.Println(f(op, 10, 5))
	}
}

おわりに

Go の実践テクニック本として『みんなのGo言語』があります。
第3章「実用的なアプリケーションを作るために」 では, 外部と通信するようなアプリケーションの開発において必要となるタイムアウトやシグナルハンドリングの部分が参考になりました。
また, 第5章「The Dark Arts Of Reflection」では, LLでランタイムが柔軟に対応するな場面で Go はコンパイル言語という制約の中で reflect を上手く使うためのポイントが勉強になりました。

[1] How to stop json.Marshal from escaping < and >?
[2] Go言語で幸せになれる10のテクニック
[3] InterfaceSlice
[4] build-web-application-with-golang
[5] Go map of functions