emahiro/b.log

Drastically Repeat Yourself !!!!

structのomitemptyの挙動と使い所の検討

golangのstructでjsonのencodingのためのpropertyに omitempty をつけた時の挙動とその使いどこを検討します。

omitemptyタグとは

https://golang.org/pkg/encoding/json/#Marshal には以下の用に記載されている。

The "omitempty" option specifies that the field should be omitted from the encoding if the field has an emptyvalue, 
defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string. 

omitempty属性をつけたstructのjsonのpropertyははその値がfalse, 0, nilのpointer,長さが0の文字列と配列、sliceの時はjsonにencodeされるときに、propertyごと省略される

type Person struct {
  Name string `json:"name"`
  Age  int64 `json:"age,omitempty"` //省略可能
}

上記hのようなstructを考える。

jon := Person {
  Name: "Jon",
  Age: 20
}

// jsonにencodeしたときのoutput
/*
{
  "name": "jon",
  "age" : 20
}
*/

taro := Person {
  Name: "taro",
}

// jsonにencodeしたときのoutput
/*
{
  "name": "taro"
}
*/

jsonにencodeしたときに omitempty 属性をつけておくとstructのinstance化のときに指定したなかったkeyについてはencodeしたjson文字列のpropertyとして含めない(省略される)。
ここまでは一般的なgoのencoding/jsonに関する知識。

ではこのomitemptyを実際のプロジェクトに使うときの注意点について検討する

omitemptyの使い所の検討

使い所はどこなのか。

omitempty 属性を使ういいところは必要ないstructのkeyをjsonにencodeしたときに自動で省略してくれるところであるので、例えば特定のリクエストのクエリパラメーターの値からjsonを生成して、別のAPIにPOSTするような動作を考えたときに、パラメータの有無によって生成するstructを返るなどの手間がいらなくなると思います。

具体的には以下のようなtokenと送信元をクエリパラメーターにタグとしてくっつけて、POSTした先でtokenと送信元をロギングするような挙動があったとします。

curl -i https:sample.com/user/1?token=hogehoge&from=yahoo.co.jp

こんなURLを叩くと以下のようなhandlerにリクエストが入ってくるとします。

package handler

type Logging struct {
  Token string `json: "token"`
  From  string `json: "from, omitempty"`
}

func SenderLogging(w http.ResponseWriter, r *http.Request) {
  values := r.URL.Query()
  token := values.Get("token")
  from := values.Get("from")
  
  // tokenもfromもクエリパラメーターがなければ空文字が入る。
  logging := Logging{
    Token: token,
    From:  from,
  }
  
  b, _ := json.Marshal(&logging)
  
  // encodeされたjsonのbyte配列になる。
}

このとき from が指定されたリクエストの時は from に値が入ったjsonが生成され、from が指定されない(直叩きされた)場合は from のpropertyが省略され、ログにもfrom要素は記録されません。 from の有無を見て、条件分岐等はする必要がありません。

omitemptyをうまく使うと、そこで差分を吸収してくれます。

懸念点

omitemptyの懸念点は上記にも記載しましたが false or "" or 0 or nil pointer などそれ自体が意味を持っていそうな値の場合も omiempty 属性が付いていることでjson化したときにkeyは省略されてしまいます。
つまり、ともすると omitempty 属性をつけていることで意図するjsonが生成されないことが想定されます。

もし、認証のプロセスで必須のパラメータに omitempty をつけていたら、何かの問題で空になってしまい、想定している正しいjsonが作られず、認証が通らない、ということが起きるかもしれません。

特に false or 0 のようなそれ自体が意味を持ちそうな値の時ですら省略されてしまうのは気をつけないと、生成されるつもりだったということが起きかねないので注意が必要です。

まとめ

omitemptyは便利な属性だけど、使い方を見誤ると想定したjsonを生成しないということになるので注意が必要です。
ただし、使い勝手のいい機能なので、ちゃんと使っていきたい。

structに埋め込んだmapの要素を上書きする

やりたいこと

struct内に埋め込んだmapのfieldの値を上書きするという処理を考えます。
下記のようなサンプルコードがあったときに originalSamples のItemsの中身を上書きするような処理です。

type Samples struct {
  Items map[string]string
}

originalSamples := Samples{
  Items: map[string]string{
    "first": "apple",
    "second": "orange",
  },
}

やることはmapの中身を更新するだけなのですが、いくつかハマったのでメモとして残して起きます。

ハマったところ

assignment to entry in nil map が発生した

mapを初期化しなかったケースです。
mapを初期化せずに新規の値をappendしようとすると panic: assignment to entry in nil map のエラーが発生します。

var m Samples

for k, item := range originalSamples.Items{
  item = "iPhone"
  m.Items[k] = item
}

この場合は var で定義するのではなく Sample{} で初期化することが必要になります。

otiai10.hatenablog.com

structを初期化してもassinment to entry in nil map が起きた

structを初期化しても同様のエラーが発生しました。
具体的なコードは以下です。

originalSamples := Samples{
  Items: map[string]string{
    "first":  "pc",
    "second": "book",
  },
}

m := Samples{}

for k, item := range originalSamples.Items {
  item = "iPhone"
  m.Items[k] = item
}

これはなぜ起きるかというと、struct内に埋め込まれているmapはstructをインスタンス化してもnilでになってしまうために assignment to entry in nil map のエラーが発生します。

こちらを回避策を検討した結果、mapをnilしないようにするので下記の2パターンが考えられると思います。

  1. mapをinstace化して後で上書きしたいstructに代入する
  2. structのinstance化と合わせて埋め込まれているmapもinstance化する
// mapをinstance化する
newSamples := Samples{}
m := make(map[string]string)

for k, item := range originalSamples.Items {
  item = "iPhone"
  m[k] = item
}

// structに代入する
newSamples.Items = m
fmt.Printf("%v", newSamples)
// structのinstance化のタイミングでmapも初期化する
newSamples := Samples{
  Items: map[string]string{},
}

for k, item := range originalSamples.Items {
  item = "iPhone"
  newSamples.Items[k] = item
}
fmt.Printf("%v", newSamples)

まとめ

mapを動的に扱いたい場合は、ちゃんと初期化されているか確認する。
structを初期化しても中のpropertyまで初期化されているわけではない。

goで任意引数でtemplateでの表示を変えたりしたい場合の考察

golangで関数に任意引数を取り、その任意引数にmapを指定することで、template場でmapで定義したinterfaceを見て表示を変えたいという実装を考えます。

やりたいこと

以下のようなことをしたいと想定します。

func RendarHTML (flags ...map["string"]interface{}){
  // flags に各種表示を分けるstatusを入れてtemplate側で制御する
  
}

golangにおける任意引数とは?

golangでは、LL言語はじめとしたオブジェクト指向言語にあるようなデフォルト引数という機能を使うことができません。
rubyとかだと

def renderHtml(templ, flag=false)
  # flagを使って何かする
end

# callする側でflagを使わないとき
renderHtml("index.html")

# flagの値を使うとき
renderHtml("detail.html", true)

上記のようにmethodに対してdefatult引数を指定して、その引数はcallする側で指定されなければdefatult引数に与えられた値がmethod内部で使用されるということが可能です。

しかし、golangではこのdefault引数を取ることはできません。
また、静的言語のため、指定した引数は必ず呼び出し側で関数にsetすることが求められます。
setしないとコンパイルが通りません。

// flagでfalseをデフォルトで指定するようなことはできない
func renderHTML(tmpl string, flag bool){ 
  // 何かしらの処理
}

// 呼び出す側

// ◯ コンパイルが通るとき
renderHTML("index.tmpl", false)

// × コンパイルが通らない
renderHTML("detail.tmpl")

// 関数に指定されている引数は満たさなければならない。

ここでgolangで使うのが 任意引数。 言葉通り、呼び出す側でsetしてもいいし、しなくてもいい任意の引数をgolangでは関数に使うことができます。
可変引数が正しい呼称らしいですね

qiita.com

任意引数を使った実装パターンの検討

まず任意引数の仕様として 任意引数をとる場合は必ずsliceとして扱う というのがあります。

func renderHTML(tmpl string, flags ...bool){
    // flagsはslice型。この場合では []book となっている。
}

// 引数をとる
renderHTML("index.tmpl", true)

// 引数をとらない
renderHTML("detail.tmpl")

// どちらでもコンパイルは正常に通ります。

しかし、引数一つでも任意引数をとっている関数内ではsliceから特定の値を取り出さないと一番最初に意図していたようなflagによってtemplateの表示を変更するような実装はできないです。 上記の例では []bool の中からお目当のstatusを持っているかを判別するような実装をすることはしません。

ここで実際に具体的な実装方法を検討します。

任意引数は関数内ではsliceとして扱われるので、flagsという直接boolを扱うことを連想させるような引数をやめて、mapをとります。
このとき関数内の変数名は、追加分なので extra のような変数名にしておくと可読性が高くなると思います。

sample実装

具体的な実装方法として、以下のようなmethodを最初考えます。

fumc renderHTML (tmpl string, extra ...map["string"]interface{}){
  // extraを使って描画する
  // extras は []map(string)interface{}
  
  RenderPage(ctx, "templete/path.tmpl",{"Extra":extra})
}

// こんな感じの引数にする
renderHTML("index.tmpl", map["string"]interface{}{"isIOS": true, "isAndroid": false})

// 引数をとらないことも可能
renderHTML("detail.tmpl")

※ 関数名は適当。

renderHTML内の引数に任意引数を使うことで、メソッドの汎用性が高くなります。
何か余分なデータや追加でデータを持たせないときはextrasに全て突っ込んで仕舞えば問題ないです。

これをtemplate側で使うにはいくつか方法があります。

  • templateでsliceのelementを扱う関数を使う
    • range
    • index
<!-- rangeで一つずつ取り出してloopを回す -->
{{ $ext := range $.Extra }}
{{ end }}

<!-- 到底のindex位置の取得する -->
{{ $ext := index $.Extra 3 }}

しかし、extraがそのまま渡されてきてtemplate側で条件分岐を使ったり、indexを制御したりするのはあまり筋がいいとは言えません。
何より、Extraに紐づく値がなければtemplateのコンパイルエラーが起きる場合もあります。

よりベターな実装方法の検討

任意のsliceで返されるextra引数をそのままとるのではなく、ルールを決めます。

  • 任意引数でとる値は必ず一つ
  • 関数側でslice型になっても、slice型のindex:0が必ずflagsをとることが決まっていれば問題ありません。
  • privateなinterfal関数を用意する

以下のような実装になります。

func renderHTML(tmpl string, extra ...map[string]interface{}){
    ext := extra[0] //先頭に来ることが確定済み
    internalRenderHTML("index.tmpl", ext) 
}

func internalRenderHTML(tmpl string, extra map[string]interface{}){
    // extraを使ってstatusを取り出す
}

任意引数のindex0番が使いたいflagなにで以下のような風に呼び出し側で使います。

renderHTML("index.tmpl", map["string"]interface{}{"IsIOS": true, "IsAndroid": false})

html側で利用するときはindexやreangeの関数を使わずとも、extraはslice型なくmapで受け取れるようにします。

{{ /* iOSモードもとき */ }}
{{ if $.Extra.IsIOS }}
{{ end }}

{{ /* Androidモードのとき */ }}
{{ if $.Extra.IsAndroid }}
{{ end }}

こういった感じでinternalで共通のmethodを呼ぶことで環境ごとにtemplateで使えるようにします。変なelement管理等は必要ないです。

任意引数でslice型をそのまま使うより単体として扱った方が使い勝手がいいですし、templateでもすぐにカンマ区切りで呼び出せるので便利だと思いました。

goのテストのカバレッジを計測する

関わっているプロダクトでテストのカバレッジ取得してみようと思ったので、標準で動作している機能を使ってgoのテストカバレッジを計測してみました。

coverageを計測する

coverageを測定するpackageを指定して、カバレッジを図るオプション -cover を指定します。

$ gotest -cover ./path/to/package_name

## test 実行中

coverage: 45.9% of statements

coverageのパーセンテージが出力されます。
goではpackage単位でカバレッジが計測されます。

テストされている箇所とされてない箇所を視認する

htmlを生成してブラウザで開く

以下の記事を参考にcover.htmlを生成してブラウザで表示させてみました。

qiita.com

GPPATH配下にプロジェクトがある場合は任意のディレクトリにて以下を叩きます。

$ go test -coverprofile=cover.out ./path/to/package
ok      ./path/to/package 0.12s  
coverage: 75.0% of statements
$ go tool cover -html=cover.out -o cover.html
$ open cover.html

ブラウザにテストされている箇所とされていない箇所が表示されます。
されている箇所は緑色で、されてない箇所は赤でコードがハイライトされているので、条件分岐箇所などをみながらどこがテストされていて、どこがテストされていないのかを確認することが可能です。

こういうツールが標準でついてるんだから本当に便利だなと思います。

テスト戦略にも関わってくるので、例えばキャッシュの有無など、動作に影響ないところのコード等はある程度テストを書かずともいいですが、ビジネスロジックに関わるところはちゃんとテストされているかを確認してみてください。

ハマったところ

direnvでプロジェクトごとにGOPATHを指定している場合に少しハマりました。 カバレッジを計測したhtmlファイルをプロジェクトとは別ディレクトリでgo tool cover -htmlをしようとしてもGOPATHが違うと言われて動作しなかったので、direnvを使っている場合は、direnvで指定したGOPATH配下に出力ファイルを作ると正常にhtmlに変換されます。

// デフォルトのGOPATHに移動
$ cd $GOPATH
$ ~/go go tool cover -html=cover.out -o cover.html
cover: can't find "application.go": cannot find package "GAE_PROJECT_GOPATH/model" in any of:
  /usr/local/Cellar/go/1.9.2/libexec/path/to/model (from $GOROOT)
  path/to/model (from $GOPATH)

ただ、gitでトラッキングする際にノイズになるので、 .gitignore~/.gitignore_global でノイズは取り払ってもおくといいかと思います。

『お金2.0』を読んだ

『お金2.0』を読んだので、ちょっとした感想をメモって起きます。

感想

帯やamazonの説明文、レビューコメントを参照した方が内容をざっと俯瞰するにはいいと思うので、ここで詳しくは載せませんが、それなりにこれからの経済のあり方がわからない人向けに丁寧に説明されている書籍でした。
普段から本書で述べられている内容に触れている身としては若干冗長さも感じつつも、改めて丁寧に説明されることで「ああ、確かに」とか「こういうことだったのか」といった知識の再確認やモヤっとしていたところが整理された感覚があり、すごくリズムよく読むことができました。
一方で「評価経済」「トークン/シェアリングエコノミー」に馴染みのない方には、正直何をいっているのか多分わからないのでは?的な感想を抱いてます。
そんなこと考えてたら、著者のTwitterでも似たようなことを呟いてて納得しました。

「お金」っていうキャッチーなフレーズを使っているので、内容が全然お金関係ないじゃんと思ったり、経済の枠組みや、歴史的背景といった内容まで一気に飛躍したりと書籍の中であっちにいったりこっちにいったりを繰り返しているのですが、今までいわば価値の王様として絶対的だった「お金」というものが、これから少しずつ変わってくるのではないか、ということが言いたかったのかなと思います。

これからは「お金」の意味も変わってくるし、国家が管理している経済以外の枠組みで色々な経済が生まれてくるだろうし、その各々の経済でエコシステムが成り立つだろうということは僕自身なんとなく感覚的には感じていました。

正直なところ。本書を読んだからといって、明日から何かが変わるとかということは一切ないと思います。
学術書ではないので、何か専門的な知識が身につくわけでもありません。
ただ、これからの経済を考える一つの観点を提供している書籍だと思います。何か新しい概念が出てきたときに、歴史からみてどうしてそういった概念、サービス、仕組みが現れてくるのか、それを俯瞰して、大きな流れの中の一つの点であるという観点を持つことができるようになるくらいのことだと思います。

お金っていう言葉が非常にキャッチーだし、内容の中でも、これからのお金の考え方そのものが変わってくるという趣旨の内容がメインなので、それにちなんだタイトルになっているのかなとも思いますが、個人的にはは「経済2.0」といってもいいのかもしれないと感じてます。
これからの経済、エコシステムを考えた時に、単一のエコシステムだけではなく、エコシステムにも多様性と、どのエコシステムで生きるのか選ぶ自由が出てくるのであろうという知識なり、一つの世界の見方、観点を持っておくことで、もし本当に本書で述べられているような時代が来たときに、面食らうことなくスムーズに受け入れることができるのではないかなーとかそんなことを感じながら本書を読み終えました。

自分でも不思議なくらい、読み終わった後に色々感が混んでしまってました。
頭の中が整理された一方で、またさらにふわふわした、モワッとした思考を延々繰り返してしまっています。

【学習】「GOならわかるシステムプログラミング」~Chapter 3~

※「GOならわかるシステムプログラミング」の3章の学習記録です。

主に

  • PNGファイルを分析してみる
  • PNGファイルに秘密のテキストを入れてみる

の2節の内容の学習記録です。

sample

sampleコードは以下

package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "hash/crc32"
    "io"
    "os"
)

const (
    OFFSET = 8
)

func dumpChunk(chunk io.Reader) {
    var l int32
    binary.Read(chunk, binary.BigEndian, &l)
    // png をbyteに変換した時の入れ物
    buf := make([]byte, 4)
    chunk.Read(buf)
    fmt.Printf("chunk '%v' (%d byte)\n", string(buf), l)
    if bytes.Equal(buf, []byte("tExt")) {
        rawText := make([]byte, l)
        chunk.Read(rawText)
        fmt.Printf("%s\n", string(rawText))
    }
}

func readChunks(file *os.File) []io.Reader {
    // pngをchunkに分けたときのchunkを格納する入れ物
    var chunks []io.Reader

    // pngの最初の8byteを飛ばして9byte目から読み込む
    file.Seek(OFFSET, 0)
    var ofs int64 = OFFSET

    for {
        // data長のいれもの
        var l int32
        err := binary.Read(file, binary.BigEndian, &l)
        if err == io.EOF {
            break
        }

        chunks = append(chunks, io.NewSectionReader(file, ofs, int64(l)+12))

        // 次のchunkの先頭に移動
        ofs, _ = file.Seek(int64(l+8), 1)
    }

    return chunks
}

func textChunk(txt string) io.Reader {
    byteData := []byte(txt)
    var buf bytes.Buffer
    binary.Write(&buf, binary.BigEndian, int32(len(byteData))) // 書き込むためのbufferを確保。長さは入力文字長。
    buf.WriteString("tExt")                                    //bufferに書き込む
    buf.Write(byteData)                                        // 入力対象の文字をbufferに書き込む
    // CRCを計算して追加
    crc := crc32.NewIEEE()
    io.WriteString(crc, "tExt")
    binary.Write(&buf, binary.BigEndian, crc.Sum32()) // crc分を新しくbufferに書き込む
    return &buf                                       // 書き込み終わったbufferを返す
}

func WriteNewTextChunk() {
    // pngをチャンクに分けて読み込み
    file, err := os.Open("./imgs/Lenna.png")
    if err != nil {
        fmt.Printf("error! err: %v", err)
        os.Exit(-1)
    }
    defer file.Close()

    newFile, _ := os.Create("./imgs/result.png")
    defer newFile.Close()

    chunks := readChunks(file)
    // pngのシグニチャー書き込み
    io.WriteString(newFile, "\x89PNG\r\n\x1a\n")

    // 先頭にIHDR chunkを書き込む
    io.Copy(newFile, chunks[0])

    // TextChunk を追加する
    io.Copy(newFile, textChunk("Test Text Chunk"))

    // 残りのチャンクを新しく追加する
    for _, c := range chunks[1:] {
        io.Copy(newFile, c)
    }
}

func main() {
    // 私いchunkを書き込んだfileを生成する
    WriteNewTextChunk()

    file, err := os.Open("./imgs/result.png")
    if err != nil {
        fmt.Printf("error! err: %v", err)
        os.Exit(-1)
    }

    chunks := readChunks(file)
    for _, c := range chunks {
        dumpChunk(c)
    }
}

githubはこちら

github.com

TOPIC

エンディアン変換について

エンディアン変換(以下 Byte Swapping)がgoには標準で装備されています。
CPUはデータの先頭には小さい桁から格納されるリトルエンディアンが採用されていますが、ネットワーク(TCP/IPプロトコル)ではデータの先頭に大きな桁から格納されるビックエンディアンが採用されています。

そのため、ビックエンディアンで格納されているネットワークで受け取ったデータをリトルエンディアンに修正する必要があります。

それが何度か出てくる以下の部分

binary.Read(r io.Reader, binary.BigEndian, data interface{})

TCP/IPプロトコルではデータは BigEndian なので、rは BigEndian で格納されているので、binary.Read するときにorderに BigEndian を指定することで、そのorderでio.Readerからデータを読み込み、dataの中に格納し直します。格納するときはリトルエンディアンに修正して格納されます。

binaryパッケージ

encoding/binary についてのメモです。

binary.Read

encoding/binary パッケージ - golang.jp

Read関数

func Read(r io.Reader, order ByteOrder, data interface{}) os.Error Readは、構造化されたバイナリデータをrから読み込みます。データは固定サイズの値へのポインタ、または固定サイズの値のスライスでなければいけません。

バイナリかされたio.Reader型のデータから、任意のdata(interface型)へ変換する

binary.Write

encoding/binary パッケージ - golang.jp

func Write(w io.Writer, order ByteOrder, data interface{}) os.Error Writeは、wへdataのバイナリ表現を書き込みます。データは固定サイズの値、または固定サイズの値へのポインタでなければいけません。

data := "Hello World"
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, int32(len(data))) // dataサイズの長さをbufに書き込む
buf.Write([]byte(data)) // 確保したbufferにdataの内容を書き込む

binary表現の書き込みがどう行われているのか、知る機械になってよかった。こういったことはシステムプログラミングをしながらわかることなので、引き続き読み進めようと思います。

json.Unmarshalではnilの参照型へのmappingはできない

httpレスポンスを構造体にmappingする際に以下のようなコードを書くケースは多々あると思います。 ※ error handlingは割愛

// responseの取得
resp, _ := http.Get(url)
body := resp.Body
defer body.Close()

// []byteへの変換
b, _ := ioutil.ReadAll(body)

var data *Data

// Data structへのjsonのmapping
if err := json.Unmarshal(b, data); err != nil{
    panic(err)
}

しかしこのコードは一見正しいように見えますが、期待する動作はしません。
以下のようなerrorが発生します。

json: Unmarshal(nil *model.Data)

参照型でもnilの構造体にはUnmarshal(復元)して構造体へのmappingはできないというエラーです。

実は json.Unmarshal(src, dist) において dist にとるべきinterface型の変数はnilではいけません。
nilではなく空の参照型の構造体を指定する必要があります。
そのため、上記のコードは以下のように書き換える必要があります。

// responseの取得
resp, _ := http.Get(url)
body := resp.Body
defer body.Close()

// []byteへの変換
b, _ := ioutil.ReadAll(body)

// ここで空の構造体を指定する。
data := &Data{}

// Data structへのjsonのmapping
if err := json.Unmarshal(b, data); err != nil{
    panic(err)
}

data := &Data{} とすることで json.Unmarshal でstrcutにmapping可能になります。

なぜこのような挙動になるかを調べました。

json.Unmarshalの公式ドキュメントによると

Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v. If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError.

と冒頭に記述されており、 If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError にある通り、「nilの参照型」or 「参照型でない」場合はErrorを返すことが公式に書かれています。

つまり、答えはまんま「nilの参照型は取れない」ということで完結します。

json.Unmarshal(src, dist) において、nilの参照型を取れないことについては以下のstackoverflowも参考になると思います。

Why does json.Unmarshal work with reference but not pointer?

エントリーの中で記述されている

To unmarshal JSON into a pointer, Unmarshal first handles the case of the JSON being the JSON literal null. In that case, Unmarshal sets the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into the value pointed at by the pointer. If the pointer is nil, Unmarshal allocates a new value for it to point to.

の中で、実際にUnmarshalメソッドがjsonを参照型に復元するときの挙動について記載してあって、これによると、unmarshalメソッドは復元プロセスの中で参照型をnilにする、しかし、復元対象の格納先の参照型の変数がすでにnilの場合、nilnilにするという挙動が発生し、そのためにerrorが発生するのだという解釈をしました。

追記

jsonのUnmarshalについてはブログの以下のエントリでも追記してます。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

2018年 ~抱負~

エンジニアとしてコミットしたと言えるプロダクトをつくる

コミットしそこねたプロダクト、コミットしたくてもできなかったプロダクト、いままでのキャリアで「これをしてきた!作ってきた!」と言える仕事に巡り合ってこなかったので、今年こそはこの目標を達成したい。
まずは今仕事で関わっているサービスにコミットすること、エンジニアとして技術でコミットすることから逃げないこと。
そして、「ただ作ること」から「よく作ること」へ自分自身の価値観のスタンダードをあげていきたいと思っている。

月2冊以上の技術書を読む

技術書を読むことは引き続き継続していきたい。
書籍の種類や内容によっては時間がかかるものがあるかもしれないけれど、今年はとにかく量を読むことに挑戦したい。
書いてあること全てを覚えなくてもいいので、量をこなして、知識のindexを色々な所に張り巡らせる1年にして行きたいと思っている。

qiita.com

githubへの1commit/weekをしていくこと

テーマ、規模は問わず、週一以上はgithubに草生やすことを目標にしたい。これの延長で何かサービスを作れたらと思っている(けどそこまで自分に期待するのはやめておく。)

github.com

新しいサービスは積極的に使っていく。

引き続き新しいサービスは積極的に触っていきたい所存。
そろそろお金がかかるサービスであっても積極的に使っていくようにしたい。これについては service safari にはいつもお世話になっているので、今年もよろしくお願いします。

www.service-safari.com

新しい言語を一つ仕事レベルで習得すること

去年はGolangを習得したので、今年も何かを実用レベルで習得したい。
今の所チェックしているのはnode。あまり触れてこなかったけど、習得していると今後サーバーサイドだけでなく、フロントエンドや、BFFといった設計にも役立ちそうななので、今年はぜひ習得したいと思っている。

とりあえず思いつく限りではこんな感じ。 技術以外では

  • 自炊のレパートリーを増やすこと
  • ブログを引き続き継続して書くこと
  • 資産運用や税制といったファイナンシャルの教養を身につけること

このあたりをテーマにやっていきたいと思う。

今年もよろしくお願いしますmm

2017年 ~振り返り~

一年も終わりなので今年の振り返りをします。
振り返り観点は以下

  • スキルスタック
  • 個人的トピック

技術スタック (個人的達成度)

Skill Commit Comment
Rails 4からあげる時に1から色々触ってました
合わせてrubyも1からいろんな書籍、資料を読んで学んだけど、いまいちrailsのエコシステムそのものには馴染めないなーと思いました。
Swift iOSとはいえ、スマホアプリ開発を理解できるようになったことはポジティブに技術の幅が広がってよかった。
とはいえ、実践で使うにはまだ稚拙...。
swift自体は非常に開発しやすくていい言語
関数型っぽいパラダイムにはいまいち馴染めないなと感じました。
Golang 今年のメイントピックの一つ
部署を異動して、Goでの開発を始めましたが、シンプルな言語仕様に加えて、エコシステムが出来上がっているので非常に馴染みやすかったです。
web系と言われるLL言語を使ってた時に感じていた課題感を大体クリアすることができていて、web系の新しいスタンダードになると思う言語だと実感しました。
ライブラリを改めて読むくせがついたことが今年の最大の進歩だと思います。(これは開発環境をIDEに移行したことも大きく寄与していると思います。)
GAE Goと並んで今年覚えたトピックの一つ。
インフラを管理しなくていい世界になったことに単純に感動しました。
このブラックボックスを毛嫌いする人はもちろんいそうだけど、個人的にはこれで十分なんじゃないかと思います。
大規模なプロジェクトでも採用実績はあるわけだし、これはインフラの第一選択肢になってしかるべきツールだと思いました。
GAEに付随して、Datastoreだったり、TaskQueueだったり、GAEの中のサービスもいくつか業務で使いながら理解することができました。
NoSQLのデータ設計はRDBMSの時とは全く違っていて、最初はとっつきづらかったです。
まだまだ深く触っていないツールもありますが、いわゆるフルマネージドな環境を使うことに慣れることができました。
API開発全般 json-schemaだったりAPIのリソース設計だったりといったことに本格的に触れました。
今までも経験してきたことではありますが、プラットフォーム開発をする上で必要となること(主に開発以外の部分ですが)を経験できたのは業務の幅が広がったと思います。

個人的トピック

好きな言語ができたこと

まだまだ経験が浅く、自分のエンジニアとしてのキャリアそのものに自信がなかったので、「言語や技術は問わない」ことを強みと思ってましたが、Goを書き始めて今まで感じていた課題感を全て真っ正面から解決されてしまい、ずっぽりこの言語にはまってしまいました。
言語にこだわりがあること自体はネガティブな捉え方をされることがしばしばありますが、もし、好きな技術を選択できるならGoを選びたいですし、働くならGoを書いて働きたいと思えるようになったは、進歩なのかもしれません。
(もちろん技術にこだわりがないことに変わりはありませんが、できることならGo書きたいなと思っています。)

個人でも何か作るときはほとんどGoを選んで使うことが多いです。

プラットフォーム開発を経験できた(現在進行形でしている)こと

プラットフォーム戦略的なことはよく聞くし、そこを目指しているサービスは星の数ほどあると思います。
今までも「〇〇のプラットフォームになる!」みたいな言葉をたくさん聞いてきたし、それは前職でも変わらずみんな言いたがることではあったと思ってます。
ただ、一開発者として、いざそれを作ろうと思って、「それ、どう作るの?」、「何が必要なの?」、みたいなことから「何をもってプラットフォームなのか?」みたいなことを全く知らなかったことに気づき、その中でプラットフォームの開発に関われたのは個人的にはすごくよかったです。
(今年発売されたこの本が非常に参考になったのは言うまでもありません。。。)

1サービスを開発しているときとは比べものにならないほどのステークホルダーの数と、調整、資料準備、コミュニケーションの数々、最初は面食らいましたが、でもこれらは全て自分がエンジニアのキャリアの中でしてこなかったことであり、今ここで身につけるべきキャリアなのかなと思って、技術以外のこともかなり時間を使っていました。

特にめちゃめちゃコンフル書いてました。ここ半年考えたら wiki:コード = 6:4くらいの比率なんじゃないかと思ってます。
多分これからもこれは続いていくんだと思います。

綺麗なコンフルの書き方とかすごい勉強しましたし、自身の文章のわかりづらさとかトンマナの不揃い感とか身にしみました。。。

ちゃんとブログ書くようになったこと

これは上に記載している、wiki力をあげる一環でちゃんと書くようにしました。
コード書く力も求められますが、文章書く力が実は地味に必要であることに、今年途中くらいから気づき始めたので、人の目に触れるものを書く練習しようと思ったのがことの発端です。

まずは、内容よりも書くことを習慣づけるために、個人の備忘録とか、ちょっと触って見たツールとかちまちま書いてました。
この傾向はしばらく続くだろうと思います笑。

ただ、アウトプットし始めたら、副産物でgithub整備するようになったし、本だけでなく、海外の記事等も時間見つけて読むように最近なってきたので、実はいい循環じゃないかと思っています。
あと、今更ですが、タイピング速度が少し上がりました。これは資料作成においては単純な効率UPに繋がってます笑。

まずは書くこと、ちゃんとした文章を素早く書くことができることを念頭においているので、あまり書いてて疲れないです。
来年はもう少しちゃんと質の高いエントリにも挑戦したいです。

総括

年度始めにアプリ方面に関わっていきたいという目標を立てたけど、結果的にはどっぷりサーバーサイド沼に浸かった1年でした。(年度って書いてあるからちょっと時期ずれます...)

ema-hiro.hatenablog.com

とにかくたくさんのことがあった1年だったし、キャリアとしては現在の開発チームに入ってからエンジニアというものを1から学び直している実感があって、しんどいことがほぼ大半でした。
3年間エンジニアを続けてきたけど、まだまだ自分なんてひよっこの部類で、しないといけないこと、できるようにならないといけないことがたくさんあって、それが実は技術に寄らないことまで広がっていて、本当に「ものつくり」は奥が深いと実感している毎日です。
技術の奥深さ、サーバーサイドの奥深さ、プラットフォーム開発の奥深さ、そしてなによりチームでのもの作りの面白さ、しんどさを痛感したので、来年はもう少し苦労しようかなと思います。

これからはgoのstreamを使っていこうと思った話

これからgoでjson形式のデータをstructにmappingする際にはstreamを使うことを決意した話です。

経緯

下記のエントリーに触発されました。

christina04.hatenablog.com

goでjsonをstructにmappingすることをはじめ、 io.Reader のデータを扱うときは一度 []byte 型に変換してから諸々の処理を行うというのが一般的な実装方法です。

一般的なjsonのmapping

APIのレスポンスなど、goにおいて io.Reader 型を受け取り、[]byte に変換してからjsonのデータ構造をオブジェクトにmappingするときの一般的なコードは下のような形式になるかと思います。

client := http.Client{}
resp, err := client.Get(url)
if err != nil {
  fmt.Printf("Error: get error. err: %v", err)
  return nil, err
}

body := resp.Body()
defer body.Close()

b, err := ioutil.ReadAll(body)
if err != nil {
  panic(err)
}

var data SomeData
if err := json.Unmarshal(b, &data); err != nil {
  panic(err)
}

一般的な上記のコードですが、byte型に変換する上で上述のエントリに記載されているようなメモリ効率が悪い点に加えて、

  • httpリクエストのとき
  • []byteに変換するとき
  • jsonをDecodeするとき

の計3回エラーハンドリングの処理を書かなければならず、コードがその分長くなるのが個人的にはちょっと冗長になっていると感じます。

streamを使う

[]byte 型に変換して json.Unmarshal する方法を使わずstreamを利用したコードは以下

client := http.Client{}
resp, err := client.Get(url)
if err != nil {
  fmt.Printf("Error: get error. err: %v", err)
  return nil, err
}

defer resp.Body.Close()
var data SomeData
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
  panic(err)
}

[]byte型に変換せずに json.NewDecoder(io.Reader) にそのまま io.Reader型のbodyを渡して、decodeします。

streamを利用すると、 []byte型に変換する箇所が丸々なくなるので、ここでエラーハンドリングの回数が一回減って、コードも見通しがよくなります。

cf. エントリ内で参照されている

stackoverflow.com

の中で言及されていますが、

So a better rule of thumb is this:

Use json.Decoder if your data is coming from an io.Reader stream, or you need to decode multiple values from a stream of data.
Use json.Unmarshal if you already have the JSON data in memory.

For the case of reading from an HTTP request, I'd pick json.Decoder since you're obviously reading from a stream.

  1. 単一のio.Readerからデータを取得した場合か単一streamにおいて複数の値をDecodeする場合 -> json.Decoderを使うべき
  2. すでにmemoryにjsonのデータを保持している場合 -> json.Unmarshal を使うべき

とされています。
APIからのレスポンスを取得するようなユースケースにおいては最初からmemoryにjsonのデータが保持されているわけではないので、 json.Decoder の方が適しているということですね。

実際に書き換えてみた

以前作った下記のリポジトリxmlをDecodeする箇所をstreamで書き換えてみました。

github.com

https://github.com/emahiro/golang-hatena-client/commit/c1ffbc8bed30a2f526d19a616dc01d360889c78b のコミットで書き換えてます。

まとめ

これからは、io.Reader をinterfaceとして持つ io.ReadCloser 型のBodyなど、streamを使える場合は積極的にstreamのまま使っていこうと思います。
また、io.Reader 型を生成して、使い回すというテクニックも覚えていこうと思います。

例えば....

  • zip.NewReadergzip.NewReader のような NewReader のインターフェースを持つ場合は積極的に、io.Reader型に変換するなど

goでAPIのスケルトンを作成する

以前書いた以下のエントリーからさらい一歩進めました。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

内容は net/httpgorilla/mux を使ってroutingを作る超薄いAPIサーバーを作成する際にこれだけコピれば簡単にapiサーバーを立てることができるみたいなものを作ることです。
rubyでいうsinatra的なものを作りたかった感じです。

net/httpを使うパターン

ディレクトリ構造

go_diet_api_skelton
  src
    go_diet_api_skelton
      handler/
        root.go
      web/
        router.go
      main.go

実際のコードは以下

github.com

/ にアクセスしてみる

$ curl -i http://localhost:808012/23 09:51:22 2017
HTTP/1.1 200 OK
Date: Sat, 23 Dec 2017 00:51:33 GMT
Content-Length: 11
Content-Type: text/plain; charset=utf-8

Hello World

goの標準機能だけで作成するどシンプルなAPIサーバーだと思います。
goの net/http が非常にシンプルで強力なので、正直これくらいシンプルな方がいいのではと思います。

gorilla/muxを使う

ディレクトリ構造

ディレクトリ構造は同じ

go_diet_api_skelton
  src
    go_diet_api_skelton
      handler/
        root.go
      web/
        router.go
      main.go

gorilla/mux を入れる。

$ dep ensure -add github/gorilla/mux

実際に書いたコードは以下

github.com

/ にアクセスしてみる

$ curl -i http://localhost:808012/23 09:51:22 2017
HTTP/1.1 200 OK
Date: Sat, 23 Dec 2017 00:51:33 GMT
Content-Length: 11
Content-Type: text/plain; charset=utf-8

Hello World

gorillaの方が以下のような感じで、routingでmethod等を指定できるので便利です。

m.HandleFunc("/", handler.Root).Methods("GET")

まとめ

以前のエントリーでは、routingまで main.go に記載していました。
しかし、main.goは薄く保ちつつ、routingの責務は router.go に負わせた方が適切だと考え、いっそのこと、汎用的に使える雛形が欲しいと思ったので、2パターンで書いてみました。

GolangではてなのRSSを取得するクライアントを作成する

仕様

  • はてなRSSのfeedのURLを登録する
  • 登録したURLでRSSを取得する
  • RSSをparseする

実装

sampleでは、はてなの「テクノロジー」のホットエントリーを使用する。 http://b.hatena.ne.jp/hotentry/it.rss

実際に叩いてみる

$ curl -i http://b.hatena.ne.jp/hotentry/it.rss
<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xmlns="http://purl.org/rss/1.0/"
 xmlns:content="http://purl.org/rss/1.0/modules/content/"
 xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
 xmlns:opensearch="http://a9.com/-/spec/opensearchrss/1.0/"
 xmlns:dc="http://purl.org/dc/elements/1.1/"
 xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#"
 xmlns:media="http://search.yahoo.com/mrss"
>
  <channel rdf:about="http://b.hatena.ne.jp/hotentry/it">
    <title>はてなブックマーク - 人気エントリー - テクノロジー</title>
    <link>http://b.hatena.ne.jp/hotentry/it</link>
    <description>最近の人気エントリー - テクノロジー</description>
    <items>
      <rdf:Seq>
        <rdf:li rdf:resource="https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4" />
        <rdf:li rdf:resource="http://tech.mercari.com/entry/2017/12/18/deadlock" />
        <rdf:li rdf:resource="http://brevis.exblog.jp/26270824/" />
        <rdf:li rdf:resource="http://www.pfu.fujitsu.com/direct/hhkb/hhkb-option/detail_pz-kbbrg.html" />
        <rdf:li rdf:resource="http://japanese.engadget.com/2017/12/18/gif-beam-amoled-24/" />
        <rdf:li rdf:resource="https://speakerdeck.com/hihihiroro/kubernetesniru-men-sitai" />
        <rdf:li rdf:resource="http://coliss.com/articles/freebies/best-free-icons-2017.html" />
        <rdf:li rdf:resource="https://note.mu/macheri_me/n/nf2447d4a6167" />
        <rdf:li rdf:resource="http://fushiroyama.hatenablog.com/entry/2017/12/17/225544" />
        <rdf:li rdf:resource="https://tech.raksul.com/2017/12/18/raksul-platform-project/" />
        <rdf:li rdf:resource="https://qiita.com/advent-calendar/2017/ubiregi" />
        <rdf:li rdf:resource="https://www.894651.com/column/craftman_004" />
        <rdf:li rdf:resource="https://anond.hatelabo.jp/20171218212227" />
        <rdf:li rdf:resource="https://forest.watch.impress.co.jp/docs/serial/yajiuma/1097447.html" />
        <rdf:li rdf:resource="https://game.watch.impress.co.jp/docs/news/1097558.html" />
        <rdf:li rdf:resource="http://www.yukisako.xyz/entry/start_arduino" />
        <rdf:li rdf:resource="http://eng-blog.iij.ad.jp/archives/1188" />
        <rdf:li rdf:resource="https://qiita.com/bokuweb/items/1575337bef44ae82f4d3" />
        <rdf:li rdf:resource="https://fullswing.dena.com/slack/" />
        <rdf:li rdf:resource="https://developers.eure.jp/tech/primer_of_sql/" />
        <rdf:li rdf:resource="https://medium.com/@deeeet/7cf4280d435b" />
        <rdf:li rdf:resource="https://shugo.net/jit/20171215.html#p01" />
        <rdf:li rdf:resource="https://gigazine.net/news/20171218-programming-atcoder/" />
        <rdf:li rdf:resource="http://www.itmedia.co.jp/news/articles/1712/18/news100.html" />
        <rdf:li rdf:resource="http://portal.nifty.com/kiji/171218201494_1.htm" />
        <rdf:li rdf:resource="https://qiita.com/sawara125/items/67a144801e3c2f09f53a" />
        <rdf:li rdf:resource="http://twitter.com/yoppymodel/status/942592739270467584" />
        <rdf:li rdf:resource="https://japan.cnet.com/article/35112083/" />
        <rdf:li rdf:resource="https://qiita.com/karupanerura/items/e90bba7166878ece9f06" />
        <rdf:li rdf:resource="https://codezine.jp/article/detail/10591" />
      </rdf:Seq>
    </items>
  </channel>
  <item rdf:about="https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4">
    <title>仮想通貨自動取引入門 - Qiita</title>
    <link>https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4</link>
    <description>本記事は U-TOKYO AP Advent Calendar 2017 の17日目です。 はじめに 年の瀬が近づき何かと出費がかさむ季節になりましたね。財布の中も真冬です。 実は2ヶ月ほど前から年越しに備えて仮想通貨で資産運用をしています。 他の資産運用と比べたときの仮想通貨取引のメリットは「少額でも大きな利益を得るチャンスがあること」と「24時間365日取引ができること」でしょうか。 というこ...</description>
    <content:encoded>&lt;blockquote cite=&quot;https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4&quot; title=&quot;仮想通貨自動取引入門 - Qiita&quot;&gt;&lt;cite&gt;&lt;img src=&quot;http://cdn-ak.favicon.st-hatena.com/?url=https%3A%2F%2Fqiita.com%2Fshionhonda%2Fitems%2Fbd2a7aaf143eff4972c4&quot; alt=&quot;&quot; /&gt; &lt;a href=&quot;https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4&quot;&gt;仮想通貨自動取引入門 - Qiita&lt;/a&gt;&lt;/cite&gt;&lt;p&gt;本記事は U-TOKYO AP Advent Calendar 2017 の17日目です。 はじめに 年の瀬が近づき何かと出費がかさむ季節になりましたね。財布の中も真冬です。 実は2ヶ月ほど前から年越しに備えて仮想通貨で資産運用をしています。 他の資産運用と比べたときの仮想通貨取引のメリットは「少額でも大きな利益を得るチャンスがあること」と「24時間365日取引ができること」でしょうか。 というこ...&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://b.hatena.ne.jp/entry/https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4&quot;&gt;&lt;img src=&quot;http://b.hatena.ne.jp/entry/image/https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4&quot; alt=&quot;はてなブックマーク - 仮想通貨自動取引入門 - Qiita&quot; title=&quot;はてなブックマーク - 仮想通貨自動取引入門 - Qiita&quot; border=&quot;0&quot; style=&quot;border: none&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://b.hatena.ne.jp/append?https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4&quot;&gt;&lt;img src=&quot;http://b.hatena.ne.jp/images/append.gif&quot; border=&quot;0&quot; alt=&quot;はてなブックマークに追加&quot; title=&quot;はてなブックマークに追加&quot; /&gt;&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;</content:encoded>
    <dc:date>2017-12-18T16:22:14+09:00</dc:date>
    <dc:subject>テクノロジー</dc:subject>
    <hatena:bookmarkcount>120</hatena:bookmarkcount>
  </item>
# 略
</rdf:RDF>

I/F

RSSを取得するだけなので、クエリパラメータでURLを渡してあげようと思います。

$ curl -i -X GET http://localhost:8080/rss?url=http://b.hatena.ne.jp/hotentry/it.rss

みたいなGetのURLを叩けばいいですね。

Parseする

構造的には以下です。

channel
  title
  link
  ...
item
item

#略

rssをParseすると言っても、実態はxmlで返ってくるデータ構造をparseすることと同じ。
以前AmazonAPIで遊んだ時と同様に、 encoding/xml を使います。 前回は愚直にxmlの構造をstructに起こしましたが、xmlをparseする時は親子構造を > 使って表現してparseすることができます。

以下のようなstructを定義してrssのfeedをマッピングします。

package model

import (
    "encoding/xml"
)

type HatenaFeed struct {
    XMLName         xml.Name         `xml:"RDF"`
    Title           string           `xml:"channel>title"`
    Link            string           `xml:"channel>link"`
    Description     string           `xml:"channel>description"`
    HatenaBookmarks []HatenaBookmark `xml:"item"`
}

type HatenaBookmark struct {
    XMLName       xml.Name `xml:"item"`
    Title         string   `xml:"title"`
    Link          string   `xml:"link"`
    Description   string   `xml:"description"`
    Content       string   `xml:"content"`
    Date          string   `xml:"date"`
    Subject       string   `xml:"subject"`
    BookmarkCount int64    `xml:"bookmarkcount"`
}

これでfeedを取得したのち、レスポンスをbyte型に変換して xml.Unmarshal でparseすることで以下のような構造のオブジェクトを取得できます。

$~/e/golang-hatena-client  go run src/golang-hatena-client/main.go                                                                                 火 12/19 03:45:02 2017
&model.HatenaFeed{
  XMLName: xml.Name{
    Space: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
    Local: "RDF",
  },
  Title:           "はてなブックマーク - 人気エントリー - テクノロジー",
  Link:            "http://b.hatena.ne.jp/hotentry/it",
  Description:     "最近の人気エントリー - テクノロジー",
  HatenaBookmarks: []model.HatenaBookmark{
    model.HatenaBookmark{
      XMLName: xml.Name{
        Space: "http://purl.org/rss/1.0/",
        Local: "item",
      },
      Title:         "仮想通貨自動取引入門 - Qiita",
      Link:          "https://qiita.com/shionhonda/items/bd2a7aaf143eff4972c4",
      Description:   "本記事は U-TOKYO AP Advent Calendar 2017 の17日目です。 はじめに 年の瀬が近づき何かと出費がかさむ季節になりましたね。財布の中も真冬です。 実は2ヶ月ほど前から年越しに備えて仮想通貨で資産運用をしています。 他の資産運用と比べたときの仮想通貨取引のメリットは「少額でも大きな利益を得るチャンスがあること」と「24時間365日取引ができること」でしょうか。 というこ...",
      Content:       "",
      Date:          "2017-12-18T16:22:14+09:00",
      Subject:       "テクノロジー",
      BookmarkCount: 174,
    },
# 略

今回の実装ではクエリパラメータに入るURLははてなのURLであればなんでも入れてRSSを取得できます。

書いたコードはこちら

github.com

goでmecabを動かす

簡単な分かち書きCLIツール作ってみようと思ったのでmecabをgoで動かして見ました。

やったこと

  • goでmecabを動かすこと

mecab-golangのinstall

以下のmecabのクライアントを使いました。

github.com

最初に

mecab をinstallし、mecab-config が入っているかを確認します。

$ brew search mecab
==> Searching local taps...
mecab
mecab-ipadic
# 以下色々
$ brew insatll mecab mecab-ipadic
$ which mecab-config
/usr/local/bin/mecab-config # 入っている!

golang-mecabを動かします。

GOPATHをリポジトリに合わせたかったので、direnvを使いました。
※ 通常はGOPATH配下にアプリケーションリポジトリを作るかもですが。

READMEに記載してあったmecabを使うためのライブラリのpathを .envrc に追記します。

export GOPATH=$(pwd)
export CGO_LDFLAGS="`mecab-config --libs`"
export CGO_CFLAGS="-I`mecab-config --inc-dir`"

depgolang-mecab を入れます。

$ dep init
$ dep ensure
$ dep ensure -add github.com/bluele/mecab-golang

これで mecab-golang が動くようになります。

実際に書いたコード

package main

import (
    "fmt"
    "os"
    "strings"

    "github.com/bluele/mecab-golang"
)

func parseToNode(m *mecab.MeCab, input string) {
    tg, err := m.NewTagger()
    if err != nil {
        fmt.Printf("NewTagger error. err: %v", err)
        os.Exit(-1)
    }
    defer tg.Destroy()

    lt, err := m.NewLattice(input)
    if err != nil {
        fmt.Printf("NewLattice error. err: %v", err)
        os.Exit(-1)
    }
    defer lt.Destroy()

    node := tg.ParseToNode(lt)
    for {
        features := strings.Split(node.Feature(), ",")
        fmt.Printf("features: %v\n", features)
        if node.Next() != nil {
            break
        }
    }
}

func main() {
    var input string
    fmt.Println("---- Input your text below ----")
    fmt.Scan(&input)
    m, err := mecab.New("-Owakati")
    if err != nil {
        fmt.Printf("Mecab instance error. err: %v", err)
    }
    defer m.Destroy()

    // parse to node
    parseToNode(m, input)

    fmt.Printf("%v", "Complete !!!")
}

動かしてみる。

$ go run main.go
---- Input your text below ----
こんにちは佐藤さん
features: [BOS/EOS * * * * * * * *]
features: [感動詞 * * * * * こんにちは コンニチハ コンニチワ]
features: [名詞 固有名詞 人名 姓 * * 佐藤 サトウ サトー]
features: [名詞 接尾 人名 * * * さん サン サン]
features: [BOS/EOS * * * * * * * *]
Complete !!!⏎

ちゃんと動いている!

今回書いたコードは以下

github.com

Amazon Product Advertising APIのレスポンスをパースする

やったこと

Amazon Product Advertising APIを使ってISBNから書籍情報を取得するプログラムを書く際に、AmazonAPIのレスポンスは json ではなく、xmlでデータが帰ってくるので、このxmlGolangでparseするためにしました。

※ 今時xmlかよ!っと思ってちょっとびっくりしたのは秘密。

Parse XML Response in Go

Golangに置いて、xmlをparseするには encoding/xml を使います。
jsonをparseするときと同様にデータ構造をstructにMappingする際に、明示的にxmlでのタグを追加します。

ex) Amazon Product Advertising API - Operation - ItemLookup の場合

type ItemLookupResponse struct {
    XMLName xml.Name `xml:"ItemLookupResponse"` // ここ!!
    Items   Items    `xml:"Items"`
}

Sample

Amazon Product Advertising APIを叩いてレスポンス(xml)を取得します。
取得の方法は以前Amazon Product Advertising APIを叩いた時に書いた以下の記事を参考にしてください。

ema-hiro.hatenablog.com

ResponseGroup は ItemAttributes を指定しています。
※ 書籍は同僚でいつもお世話になっているyoichiro san の最新の著書を使わせてもらってます。

"<?xml version=\"1.0\" ?><ItemLookupResponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\"><OperationRequest><HTTPHeaders><Header Name=\"UserAgent\" Value=\"Go-http-client/1.1\"></Header></HTTPHeaders><RequestId>e715307c-950e-4a5f-80d8-bc90d09c0ce9</RequestId><Arguments><Argument Name=\"AWSAccessKeyId\" Value=\"AKIAI43PAKG3WCIFW2DQ\"></Argument><Argument Name=\"AssociateTag\" Value=\"emahiro-22\"></Argument><Argument Name=\"IdType\" Value=\"ISBN\"></Argument><Argument Name=\"ItemId\" Value=\"9784774193328\"></Argument><Argument Name=\"Operation\" Value=\"ItemLookup\"></Argument><Argument Name=\"ResponseGroup\" Value=\"ItemAttributes\"></Argument><Argument Name=\"SearchIndex\" Value=\"Books\"></Argument><Argument Name=\"Service\" Value=\"AWSECommerceService\"></Argument><Argument Name=\"Timestamp\" Value=\"2017-12-11T17:25:44Z\"></Argument><Argument Name=\"Signature\" Value=\"Ak5Lz77ab+aGuBgz9eLuPRMqP092drq+Yqmp455DlAA=\"></Argument></Arguments><RequestProcessingTime>0.0137283900000000</RequestProcessingTime></OperationRequest><Items><Request><IsValid>True</IsValid><ItemLookupRequest><IdType>ISBN</IdType><ItemId>9784774193328</ItemId><ResponseGroup>ItemAttributes</ResponseGroup><SearchIndex>Books</SearchIndex><VariationPage>All</VariationPage></ItemLookupRequest></Request><Item><ASIN>4774193321</ASIN><DetailPageURL>https://www.amazon.co.jp/%E3%82%BD%E3%83%BC%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%A2%E3%83%97%E3%83%AA%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E6%A7%8B%E7%AF%89%E6%8A%80%E6%B3%95%E2%80%95%E2%80%95SNS%E3%81%8B%E3%82%89BOT%E3%81%BE%E3%81%A7IT%E3%82%92%E3%82%B3%E3%82%A2%E3%81%AB%E6%88%90%E9%95%B7%E3%81%99%E3%82%8B%E4%BC%81%E6%A5%AD%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-Software-Design-plus%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-%E6%B4%8B%E4%B8%80%E9%83%8E/dp/4774193321?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=165953&amp;creativeASIN=4774193321</DetailPageURL><ItemLinks><ItemLink><Description>Add To Wishlist</Description><URL>https://www.amazon.co.jp/gp/registry/wishlist/add-item.html?asin.0=4774193321&amp;SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=4774193321</URL></ItemLink><ItemLink><Description>Tell A Friend</Description><URL>https://www.amazon.co.jp/gp/pdp/taf/4774193321?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=4774193321</URL></ItemLink><ItemLink><Description>All Customer Reviews</Description><URL>https://www.amazon.co.jp/review/product/4774193321?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=4774193321</URL></ItemLink><ItemLink><Description>All Offers</Description><URL>https://www.amazon.co.jp/gp/offer-listing/4774193321?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=4774193321</URL></ItemLink></ItemLinks><ItemAttributes><Author>田中 洋一郎</Author><Binding>単行本(ソフトカバー)</Binding><EAN>9784774193328</EAN><EANList><EANListElement>9784774193328</EANListElement></EANList><IsAdultProduct>0</IsAdultProduct><ISBN>4774193321</ISBN><Label>技術評論社</Label><Languages><Language><Name>日本語</Name><Type>Published</Type></Language></Languages><Manufacturer>技術評論社</Manufacturer><NumberOfPages>360</NumberOfPages><PackageDimensions><Height Units=\"100分の1インチ\">71</Height><Length Units=\"100分の1インチ\">835</Length><Weight Units=\"100分の1ポンド\">97</Weight><Width Units=\"100分の1インチ\">591</Width></PackageDimensions><ProductGroup>Book</ProductGroup><ProductTypeName>ABIS_BOOK</ProductTypeName><PublicationDate>2017-10-20</PublicationDate><Publisher>技術評論社</Publisher><Studio>技術評論社</Studio><Title>ソーシャルアプリプラットフォーム構築技法――SNSからBOTまでITをコアに成長する企業の教科書 (Software Design plusシリーズ)</Title></ItemAttributes></Item><Item><ASIN>B076GXMNFN</ASIN><DetailPageURL>https://www.amazon.co.jp/%E3%82%BD%E3%83%BC%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%A2%E3%83%97%E3%83%AA%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E6%A7%8B%E7%AF%89%E6%8A%80%E6%B3%95-%E2%80%95%E2%80%95SNS%E3%81%8B%E3%82%89BOT%E3%81%BE%E3%81%A7IT%E3%82%92%E3%82%B3%E3%82%A2%E3%81%AB%E6%88%90%E9%95%B7%E3%81%99%E3%82%8B%E4%BC%81%E6%A5%AD%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-Software-Design-plus-ebook/dp/B076GXMNFN?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=165953&amp;creativeASIN=B076GXMNFN</DetailPageURL><ItemLinks><ItemLink><Description>Add To Wishlist</Description><URL>https://www.amazon.co.jp/gp/registry/wishlist/add-item.html?asin.0=B076GXMNFN&amp;SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=B076GXMNFN</URL></ItemLink><ItemLink><Description>Tell A Friend</Description><URL>https://www.amazon.co.jp/gp/pdp/taf/B076GXMNFN?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=B076GXMNFN</URL></ItemLink><ItemLink><Description>All Customer Reviews</Description><URL>https://www.amazon.co.jp/review/product/B076GXMNFN?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=B076GXMNFN</URL></ItemLink><ItemLink><Description>All Offers</Description><URL>https://www.amazon.co.jp/gp/offer-listing/B076GXMNFN?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=B076GXMNFN</URL></ItemLink></ItemLinks><ItemAttributes><Author>田中 洋一郎</Author><Binding>Kindle版</Binding><EISBN>9784774193687</EISBN><Format>Kindle本</Format><IsAdultProduct>0</IsAdultProduct><Label>技術評論社</Label><Languages><Language><Name>日本語</Name><Type>Published</Type></Language></Languages><Manufacturer>技術評論社</Manufacturer><NumberOfPages>602</NumberOfPages><ProductGroup>eBooks</ProductGroup><ProductTypeName>ABIS_EBOOKS</ProductTypeName><PublicationDate>2017-10-20</PublicationDate><Publisher>技術評論社</Publisher><ReleaseDate>2017-10-20</ReleaseDate><Studio>技術評論社</Studio><Title>ソーシャルアプリプラットフォーム構築技法 ――SNSからBOTまでITをコアに成長する企業の教科書 Software Design plus</Title></ItemAttributes></Item></Items></ItemLookupResponse>"

ItemAttributesResponseGroup として指定した時の上記のようなxmlが返ってきても、生のxmlは可読性が悪いので、以下のようなサービスを使ってresponseのxmlを見やすい形に整形して、一つ一つ構造体にmappingしていきます。

http://u670.com/pikamap/htmlseikei.php

<?xml version=\"1.0\" ?>
<ItemLookupResponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">
  <OperationRequest>
    <HTTPHeaders>
      <Header Name=\"UserAgent\" Value=\"Go-http-client/1.1\"></Header>
    </HTTPHeaders>
    <RequestId>e7d362b0-abe9-42a1-9cc1-ef9943303e12</RequestId>
    <Arguments>
      <Argument Name=\"AWSAccessKeyId\" Value=\"AKIAI43PAKG3WCIFW2DQ\"></Argument>
      <Argument Name=\"AssociateTag\" Value=\"emahiro-22\"></Argument>
      <Argument Name=\"IdType\" Value=\"ISBN\"></Argument>
      <Argument Name=\"ItemId\" Value=\"9784774193328\"></Argument>
      <Argument Name=\"Operation\" Value=\"ItemLookup\"></Argument>
      <Argument Name=\"ResponseGroup\" Value=\"ItemAttributes\"></Argument>
      <Argument Name=\"SearchIndex\" Value=\"Books\"></Argument>
      <Argument Name=\"Service\" Value=\"AWSECommerceService\"></Argument>
      <Argument Name=\"Timestamp\" Value=\"2017-12-12T16:28:21Z\"></Argument>
      <Argument Name=\"Signature\" Value=\"MGrC8pmelta5xLNmwDTSH3HdmksJvE5PieDTu9lOqzE=\"></Argument>
    </Arguments>
    <RequestProcessingTime>0.0108954000000000</RequestProcessingTime>
  </OperationRequest>
  <Items>
    <Request>
      <IsValid>True</IsValid>
      <ItemLookupRequest>
        <IdType>ISBN</IdType>
        <ItemId>9784774193328</ItemId>
        <ResponseGroup>ItemAttributes</ResponseGroup>
        <SearchIndex>Books</SearchIndex>
        <VariationPage>All</VariationPage>
      </ItemLookupRequest>
    </Request>
    <Item>
      <ASIN>4774193321</ASIN>
      <DetailPageURL>https://www.amazon.co.jp/%E3%82%BD%E3%83%BC%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%A2%E3%83%97%E3%83%AA%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E6%A7%8B%E7%AF%89%E6%8A%80%E6%B3%95%E2%80%95%E2%80%95SNS%E3%81%8B%E3%82%89BOT%E3%81%BE%E3%81%A7IT%E3%82%92%E3%82%B3%E3%82%A2%E3%81%AB%E6%88%90%E9%95%B7%E3%81%99%E3%82%8B%E4%BC%81%E6%A5%AD%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-Software-Design-plus%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-%E6%B4%8B%E4%B8%80%E9%83%8E/dp/4774193321?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=165953&amp;creativeASIN=4774193321</DetailPageURL>
      <ItemLinks>
        <ItemLink>
          <Description>Add To Wishlist</Description>
          <URL>https://www.amazon.co.jp/gp/registry/wishlist/add-item.html?asin.0=4774193321&amp;SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=4774193321</URL>
        </ItemLink>
        <ItemLink>
          <Description>Tell A Friend</Description>
          <URL>https://www.amazon.co.jp/gp/pdp/taf/4774193321?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=4774193321</URL>
        </ItemLink>
        <ItemLink>
          <Description>All Customer Reviews</Description>
          <URL>https://www.amazon.co.jp/review/product/4774193321?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=4774193321</URL>
        </ItemLink>
        <ItemLink>
          <Description>All Offers</Description>
          <URL>https://www.amazon.co.jp/gp/offer-listing/4774193321?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=4774193321</URL>
        </ItemLink>
      </ItemLinks>
      <ItemAttributes>
        <Author>田中 洋一郎</Author>
        <Binding>単行本(ソフトカバー)</Binding>
        <EAN>9784774193328</EAN>
        <EANList>
          <EANListElement>9784774193328</EANListElement>
        </EANList>
        <IsAdultProduct>0</IsAdultProduct>
        <ISBN>4774193321</ISBN>
        <Label>技術評論社</Label>
        <Languages>
          <Language>
            <Name>日本語</Name>
            <Type>Published</Type>
          </Language>
        </Languages>
        <Manufacturer>技術評論社</Manufacturer>
        <NumberOfPages>360</NumberOfPages>
        <PackageDimensions>
          <Height Units=\"100分の1インチ\">71</Height>
          <Length Units=\"100分の1インチ\">835</Length>
          <Weight Units=\"100分の1ポンド\">97</Weight>
          <Width Units=\"100分の1インチ\">591</Width>
        </PackageDimensions>
        <ProductGroup>Book</ProductGroup>
        <ProductTypeName>ABIS_BOOK</ProductTypeName>
        <PublicationDate>2017-10-20</PublicationDate>
        <Publisher>技術評論社</Publisher>
        <Studio>技術評論社</Studio>
        <Title>ソーシャルアプリプラットフォーム構築技法――SNSからBOTまでITをコアに成長する企業の教科書 (Software Design plusシリーズ)</Title>
      </ItemAttributes>
    </Item>
    <Item>
      <ASIN>B076GXMNFN</ASIN>
      <DetailPageURL>https://www.amazon.co.jp/%E3%82%BD%E3%83%BC%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%A2%E3%83%97%E3%83%AA%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E6%A7%8B%E7%AF%89%E6%8A%80%E6%B3%95-%E2%80%95%E2%80%95SNS%E3%81%8B%E3%82%89BOT%E3%81%BE%E3%81%A7IT%E3%82%92%E3%82%B3%E3%82%A2%E3%81%AB%E6%88%90%E9%95%B7%E3%81%99%E3%82%8B%E4%BC%81%E6%A5%AD%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-Software-Design-plus-ebook/dp/B076GXMNFN?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=165953&amp;creativeASIN=B076GXMNFN</DetailPageURL>
      <ItemLinks>
        <ItemLink>
          <Description>Add To Wishlist</Description>
          <URL>https://www.amazon.co.jp/gp/registry/wishlist/add-item.html?asin.0=B076GXMNFN&amp;SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=B076GXMNFN</URL>
        </ItemLink>
        <ItemLink>
          <Description>Tell A Friend</Description>
          <URL>https://www.amazon.co.jp/gp/pdp/taf/B076GXMNFN?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=B076GXMNFN</URL>
        </ItemLink>
        <ItemLink>
          <Description>All Customer Reviews</Description>
          <URL>https://www.amazon.co.jp/review/product/B076GXMNFN?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=B076GXMNFN</URL>
        </ItemLink>
        <ItemLink>
          <Description>All Offers</Description>
          <URL>https://www.amazon.co.jp/gp/offer-listing/B076GXMNFN?SubscriptionId=AKIAI43PAKG3WCIFW2DQ&amp;tag=emahiro-22&amp;linkCode=xm2&amp;camp=2025&amp;creative=5143&amp;creativeASIN=B076GXMNFN</URL>
        </ItemLink>
      </ItemLinks>
      <ItemAttributes>
        <Author>田中 洋一郎</Author>
        <Binding>Kindle版</Binding>
        <EISBN>9784774193687</EISBN>
        <Format>Kindle本</Format>
        <IsAdultProduct>0</IsAdultProduct>
        <Label>技術評論社</Label>
        <Languages>
          <Language>
            <Name>日本語</Name>
            <Type>Published</Type>
          </Language>
        </Languages>
        <Manufacturer>技術評論社</Manufacturer>
        <NumberOfPages>602</NumberOfPages>
        <ProductGroup>eBooks</ProductGroup>
        <ProductTypeName>ABIS_EBOOKS</ProductTypeName>
        <PublicationDate>2017-10-20</PublicationDate>
        <Publisher>技術評論社</Publisher>
        <ReleaseDate>2017-10-20</ReleaseDate>
        <Studio>技術評論社</Studio>
        <Title>ソーシャルアプリプラットフォーム構築技法 ――SNSからBOTまでITをコアに成長する企業の教科書 Software Design plus</Title>
      </ItemAttributes>
    </Item>
  </Items>
</ItemLookupResponse>

構造化されたxmlのレスポンスを取得することができました。 このようなxmlの構造をstructにMappingするために書いたコードが以下にあります。

github.com

ハマったところ

API周りだと、ResponseGroupをいくつか追加で指定することができますが、複数指定する場合には , の後に 半角スペース を入れてはいけということを知りました。
そのため、Itemの情報(ItemAttributes)だけでなく、Itemの画像を知りたい時は Images というResponseGroupを追加することになるのですが、

// OK
params.Set("ResponseGroup", "ItemAttributes,Images")

// NG 
params.Set("ResponseGroup", "ItemAttributes, Images") // 半角スペースが入ってしまっている。

となります。

また、xmlの構造をStructにMappingする時に

type ItemAttributes struct {
    XMLName           xml.Name          `xml:"ItemAttributes"`
    Author            []string          `xml:"Author"`
    Binding           string            `xml:"Binding"`
    EAN               int64             `xml:"EAN"`
    EANList           EANList           `xml:"EANList"`
    IsAdultProduct    bool              `xml:"IsAdultProduct"`
    ISBN              string            `xml:"ISBN"`
    Label             string            `xml:"Label"`
    Languages         Languages         `xml:"Languages"`
    Manufacturer      string            `xml:"Manufacturer"`
    NumberOfPages     int64             `xml:"NumberOfPages"`
    PackageDimensions PackageDimensions `xml:"PackageDimensions"`
    ProductGroup      string            `xml:"ProductGroup"`
    ProductTypeName   string            `xml:"ProductTypeName"`
    PublicationDate   string            `xml:"PublicationDate"`
    Publisher         string            `xml:"Publisher"`
    Studio            string            `xml:"Studio"`
    Title             string            `xml:"Title"`
}

のような構造において Languages fieldを考える時に、通常なら

type Language struct {
    XMLName xml.Name `xml:"Language"`
    Name    string   `xml:"Name"`
    Type    string   `xml:"Type"`
}

という Language のStruct(Model)を用意して、

// 略
Languages         []Language         `xml:"Languages"`

のように、明示的に配列がデータ構造としてはいることがわかりそうなものですが、上記の記載方法だとUnmarshalする時にエラーになってしまいます。
xmlをParseする際には、以下のような中間Structを定義しないと正確にUnmarshalされないことを知りました。

type Languages struct {
    XMLName  xml.Name   `xml:"Languages"`
    Language []Language `xml:"Language"`
}

Languages タグはあくまで Languages タグであり、その中に Language が配列として入っている、というタグの構造を理解せず、 JsonをMappingするときの感覚だと失敗しました。

追記

xml> を使って親子関係を表現できます。
ItemAttributes のstructの構造において

EANListの構造は以下

EANList
  EANListElement

Languagesの構造は以下

Languages
  []Language
  

という構造を表現するのにstructを二つ作って親子関係を表現していました。 しかし、 > を使って xmlの定義の箇所で親子関係を表現させることが可能です。

なので ItemAttributes のstructは以下のように変更可能になります。

package model

import "encoding/xml"

type ItemAttributes struct {
    XMLName           xml.Name          `xml:"ItemAttributes"`
    Author            []string          `xml:"Author"`
    Binding           string            `xml:"Binding"`
    EAN               int64             `xml:"EAN"`
    EANList           int64             `xml:"EANList>EANListElement"` // ここ
    IsAdultProduct    bool              `xml:"IsAdultProduct"`
    ISBN              string            `xml:"ISBN"`
    Label             string            `xml:"Label"`
    Languages         []Language        `xml:"Languages>Language"` // ここ
    Manufacturer      string            `xml:"Manufacturer"`
    NumberOfPages     int64             `xml:"NumberOfPages"`
    PackageDimensions PackageDimensions `xml:"PackageDimensions"`
    ProductGroup      string            `xml:"ProductGroup"`
    ProductTypeName   string            `xml:"ProductTypeName"`
    PublicationDate   string            `xml:"PublicationDate"`
    Publisher         string            `xml:"Publisher"`
    Studio            string            `xml:"Studio"`
    Title             string            `xml:"Title"`
}

type Language struct {
    XMLName xml.Name `xml:"Language"`
    Name    string   `xml:"Name"`
    Type    string   `xml:"Type"`
}

type PackageDimensions struct {
    XMLName xml.Name `xml:"PackageDimensions"`
    Height  int64    `xml:"Height"`
    Length  int64    `xml:"Length"`
    Weight  int64    `xml:"Weight"`
    Width   int64    `xml:"Width"`
}

xmlは親子関係をgolangで柔軟に表現できるので使い勝手がいいと感じました。

Amazon Product Advertising APIを使ってISBNコードから書籍情報を取得する

やったこと

  • Amazon Product Advertising APIを使って書籍情報を取得すること
  • 書籍情報の検索フックにはISBNコードを使ったこと
  • 利用する際にハマったこと

事前準備

以下は事前にやってあるものとします。

  • Amazon Associate への登録
    • Associate TagにこのIDを使う
  • AcessKeyIdとAccessSecretIdを使う

refs:

blog.apitore.com

実装

※ 書籍のAPIを取得するところのみ抜粋

ディレクトリ構造

- PROJECT
  - conf
    - token_cred.json
  - src
    - project
      - main.go
      - handler/
          - amazon.go

amazon.go の実装は下記

const (
    EC_SERVICE_ENDPOINT = "webservices.amazon.co.jp"
    EC_SERVICE_URI      = "/onca/xml"
)

func readConf() ([]byte, error) {
    f, err := os.Open("./conf/token_cred.json")
    if err != nil {
        fmt.Printf("token_cred.json open error: err; %v", err)
        return nil, err
    }
    b, err := ioutil.ReadAll(f)
    if err != nil {
        fmt.Printf("json file read error: err; %v", err)
        return nil, err
    }
    return b, nil
}

func SearchISBN(w http.ResponseWriter, r *http.Request) {
    b, err := readConf()
    if err != nil {
        fmt.Printf("readConf error. err: %v", err)
        return
    }

    var cred model.AmazonTokenCred
    if err := json.Unmarshal(b, &cred); err != nil {
        fmt.Printf("json unmarshal error. err: %v", err)
        return
    }

    params := url.Values{}
    params.Set("Service", "AWSECommerceService")
    params.Set("Operation", "ItemLookup")
    params.Set("ItemId", "ISBNコード")
    params.Set("IdType", "ISBN")
    params.Set("SearchIndex", "Books")
    params.Set("Timestamp", time.Now().UTC().Format(time.RFC3339))
    params.Set("AWSAccessKeyId", cred.AccessKeyId)
    params.Set("AssociateTag", cred.AssociateTag)
    params.Set("ResponseGroup", "Images,ItemAttributes,Offers")

    // 署名
    canonical_params := params.Encode()
    strToSign := fmt.Sprintf("GET\n%v\n%v\n%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params)
    mac := hmac.New(sha256.New, []byte(cred.SecretKeyId))
    mac.Write([]byte(strToSign))
    signature := url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
    canonical_params = fmt.Sprintf("%v&Signature=%v", canonical_params, signature)

    // http request
    res, err := http.Get(fmt.Sprintf("http://%v%v?%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params))
    if err != nil {
        fmt.Printf("response error. err: %v", err)
        return
    }
  // response はよしなに整形する
}

ハマったポイント

APIのDocs(https://images-na.ssl-images-amazon.com/images/G/09/associates/paapi/dg/index.html?RG_ItemAttributes.html の例のところ)に書いてある遠りのコード書いたら

  • The request must contain the parameter Timestamp.
  • The request must contain the parameter Signature.

の2つのエラーにぶち当たりました。
REST APIのドキュメントをそのまま叩いたのは以下

 curl -i "http://ecs.amazonaws.com/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=XXXXXX&Operation=ItemLookup&ItemId=B00008OE6I"
HTTP/1.1 400 Bad Request
Date: Fri, 08 Dec 2017 19:10:17 GMT
Server: Apache-Coyote/1.1
Vary: Accept-Encoding,User-Agent
nnCoection: close
Transfer-Encoding: chunked

<?xml version="1.0"?>
<ItemLookupErrorResponse xmlns="http://ecs.amazonaws.com/doc/2005-10-05/"><Error><Code>MissingParameter</Code><Message>The request must contain the parameter Signature.</Message></Error><RequestID>831754e7-5761-4d0a-adae-6febc205949b</RequestID></ItemLookupErrorResponse>⏎

TimeStampについて

今の時刻をISO8601の形式で使います。
GOにおいては time.RFC3339 で求められてる形式で時刻を取得します。

署名(Signature)について

AmazonProduct Advertising APIで使用する署名(Signature)は 発行したSecretKeyIdを使ってhmacでハッシュ化されたものをbase64でencodeした値 です。

署名の作成は以下のphpのコードを参考にgoで書き直しました。

qiita.com

qiita.com

githubに以下の今回使おうとしているAPIのgoのクライアントを見つけたのでこちらも参考しました。

github.com

署名作成でハマったこと

書いたコードの中で署名を生成している箇所は以下

// 署名
canonical_params := params.Encode()
strToSign := fmt.Sprintf("GET\n%v\n%v\n%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params)
mac := hmac.New(sha256.New, []byte(cred.SecretKeyId))
mac.Write([]byte(strToSign))
signature := url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
canonical_params = fmt.Sprintf("%v&Signature=%v", canonical_params, signature)

url.Values でクエリパラメータをセットして Encode したら完了かと思っていたのですが、上記の署名生成のコードの最後の行で記載しているように Signatureは最後につけないといけない というところで思いっきりハマりました。

例は以下

# params.Encode()でqueryparamsを作成したときのクエリパラメータ
"AWSAccessKeyId=[AccessKeyId]&AssociateTag=[AssociateTag]&IdType=ISBN&ItemId=9784774193328&Operation=ItemLookup&ResponseGroup=Images,ItemAttributes,Offers&SearchIndex=Books&Service=AWSECommerceService&Signature=[生成した署名]&Timestamp=2017-12-10T06:16:16Z"
# -> 403 Forbidden

# 文字列結合でsignatureをつけたときのクエリパラメータ
"AWSAccessKeyId=[AccessKeyId]&AssociateTag=[AssociateTag]&IdType=ISBN&ItemId=9784774193328&Operation=ItemLookup&ResponseGroup=Images,ItemAttributes,Offers&SearchIndex=Books&Service=AWSECommerceService&Timestamp=2017-12-10T06:16:16Z&Signature=[生成した署名]"
# status OK

当初 params.Encode() で文字列作成をしてし待ってましたが、params.Encode() はqueryのkeyを自動でsortしてしまい、 Signature が末尾に来ません。 そのため、params.Encode() でクエリパラメータを生成し、署名内容とurlは文字列結合の時と同じでも、403を返して来てしまいました。

まとめ

Amazon Product Advertising APIは署名を自前で作らないといけなかったりして忘れかけたことを思い出すきっかけをくれたので久しぶりに触ってみてよかったです。
APIのテストで自動的にphpjavaのコードは生成してくれますが、こういうときにgoとかで書き直してみるのもいいなと思います。

とりあえず書いたコードはこちら

github.com

xmlのレスポンスをstructにマッピングしたりはまた次回!