emahiro/b.log

Drastically Repeat Yourself !!!!

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にマッピングしたりはまた次回!

os.Openするときの相対パスの書き方

goで os.Open(filename string) を実行するときに読み取りファイルを特定のディレクトリにまとめておいたときに os.Open で指定する相対パスがどう決まるかのメモです。

答えはmain.goからの相対パスになります。

例えば以下のようなディレクトリ構成のとき

- src
  - main.go
  - conf.json

conf.jsonを読み込むためには

f, err := os.Open("./conf.json")

となります。

次に以下のようなディレクトリ構成を考えます。

- Project
  - conf
    - conf.json
  - src
    - project
      - main.go

この場合は

f, err := os.Open("../../conf/conf.json")

となります。

main.goから見た相対パスだということを忘れていたので備忘録のために書きました。

[追記]

答えはmain.goからの相対パスになります。

ここが少し違っていてこれは実行ディレクトリから見たときの相対パスになります。

- Project
  - conf
    - conf.json
  - src
    - project
      - main.go

のようなディレクトリ構造の時

# ProjectRootで実行する
$ go run src/project/main.go

とする場合はconf以下のjsonを読み込もうと思ったら os.Open(./conf/conf.json) になります。
一方で

# projectディレクトリで実行する
$ go run main.go

と実行する場合は本エントリで書いたような相対パスになります。

どこで実行するかで書き方を変更する必要があります。

選んだ理由よりも「選ばなかった」理由を知りたいなという話

現在働いているチームの目指すべき姿の一つに、答えではなく「観点」を理解する ことで次回以降一人でその答えに辿り着けるようにする。というものがあります。 自分はこの言葉がすごく好きではあるのですが、答えにたどり着く観点の他に、その答えの他に考えていたいくつかの「答え」候補を選ばなかった理由も合わせて知ることが、考える力と判断する力を養う一つのアプローチになるのではないかと考えています。

選ばなかった理由を知ることの例えとして、技術選定のプロセスを考えます。
このプロセスでは 特定の技術を採用する というのが最終的なゴールになります。 特定の技術を採用するということは、候補として上がっていた他の技術を採用しなかった ということになるわけで、その技術たちを どうして採用しなかったのか ということの方が、採用した経緯を知るより学べる事が多いと感じています。

もう一つの例えとしてコードを書くことを考えて見ます。

「コードを書く瞬間の思考」にアドバイスを貰える

at-grandpa.hatenablog.jp

書かなかったコードや、なぜそれを残さなかったのかにも学びがある

コーディングにおいてはもっとわかりやすいですが、「書かなかった理由 = 選ばなかった理由」であり、できる人のコードの書く瞬間が一番勉強になります。
現に自分であれば書いたであろう1行を書かないわけですから、その意図や理由は非常に勉強になります。

技術選定にしろ、コードを書くことにろ、結局は「課題解決」の手段として捉えればできる人の意思決定を真似るには できる人が選ばないことを自分も選ばなければいい ということに落ち着くのではないかと考えています。

ある特定の問題解決を使用する際に、いくつかソリューションの候補はあげますが、いざ決める時にその人の「思考」が詰まっています。
その思考のことをノウハウというのではないかと考えてるようになりました。

何かの意思決定をすることは することを決める ことですが、これは しないことを決める と同義です。
しかしながら、することを決める ノウハウは様々な場で知見が共有される一方で、しないことを決める ノウハウは語られることは少なく、そもそも語られなかったりして、「なぜ選ばなかったのか?」「なぜしなかったのか?」ということはあまり知見として広まっていないように感じています。

意思決定の瞬間の思考を深掘りするときに、どうして選ばなかったのかをしっかり説明できるようになりたいし、意思決定する際には「しない」理由にも目を向けると、自身の価値判断の手数が増えていき、武器が増えていくように感じます。

たくさんの選ばない理由に触れ、吸収し続けることの大事さを最近感じる出来事があったので、備忘録としてまとめて見ました。

おわり

depでginを入れる

やったこと

depをつかってginをinstallして動かすまで。

depのinstall方法

以下を見て下さい。

ema-hiro.hatenablog.com

depでginを入れる

depでginをinstallします。

# projectのROOTにいるとする。
$ cd ./src/{project_name}
$ dep init
$ dep ensure -add github.com/gin-gonic/gin

終わり...

のはずだった。

しかし、goファイルがないよ!!! というエラーが出る事が分かったので、src/{project_name} 直下に main.go を置いてpackage mainを書いて再度 dep ensure -add をすることでginをDLする事ができます。

go ✕ ajaxを書いてみた

サマリ

goで簡易的なajax通信するアプリを作ったのでそのメモ

構成

環境

ディレクトリ構成

- src
  - app
    - main.go
    - handler
      - handler.go
    - render
      - render.go
- templates
  - index.tmpl

sampleコード

main.go

var port = "8080"
func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", handler.Top).Methods("GET")
    {
        router.HandleFunc("/sample", handler.SamplePost).Methods("POST")
    }
    if err := http.ListenAndServe(fmt.Sprintf(":%s", port), router); err != nil {
        log.Fatalf("err: %v", err)
    }
}

handler/handler.go

func SamplePost(w http.ResponseWriter, r *http.Request) {
    // postのリクエストを処理する
  client := http.Client{}
  req, err := http.NewRequest("POST", "http://sample.com/create", nil)
    if err != nil {
        log.Fatalf("build request error. err: %v", err)
    }
    // form をparseする
    if err := r.ParseForm(); err != nil {
        log.Fatalf("parse form error. err: %v", err)
    }

    // 何かしらrequestのbodyにparseしたURLのパラメータを入れ込む処理か何かが入る
  /*
    何かしらの処理
  */
  
    resp, err := client.Do(req)
    if resp.Status != "200 OK" {
        log.Fatalf("http request failed. code: %v", resp.Status)
    }
  json.NewEncoder(w).Encode(&resp)
}

index.tmpl のjsの部分

var url = $("form").attr("action");
var p = $("form").serialize()
$.ajax({
  url: url,
  type: "post",
  data: p,
}).done(function (data) {
  res = JSON.parse(data);
  // responseで返ってきたjsonを◯◯する
}).fail(function (err) {
  console.log(err)
});

ハマったところ

requestはそのままparseする

簡易的なajaxなので今回はjqueryを使ってさらっと書いてますが、goでPOSTリクエストを受けて、serializedされたパラメータを url.Values 型として扱うためには、ポインタとして受け渡される http.Request をそのままparseします。

// form をparseする
    if err := r.ParseForm(); err != nil {
        log.Fatalf("parse form error. err: %v", err)
    }

http.Request の持つ関数の中に PostFrom というのがありますが、これは新しく空の url.Values を作るだけで、フロントからリクエストされたrequestをparseしてくれるわけではないです。

直感的な名前がついていたために、間違って使っていて、どうしてもrequestを一度でparseできないと悩んでしまっていました。

まとめ

validationとか一切考えない超簡易的なajaxを書いてみましたが、標準のライブラリしか使っていないのに、案外簡単に書けました。

GoLandの設定をremoteで管理する

Golandの設定をremoteで管理したかったので、その設定方法をメモとして書いておきます。

背景

PC変えたりすると使っていたPCの設定が全て初期化されて1から作り直すのめんどくさいです。
PCやeditorくらいであればもしかしたら、設定ファイルをgithub等にあげて clone してくれば設定完了みたいなことはしていましたが、jetbrains系のIDEの設定までremoteで管理していなかったので、この機会にGoLandを使ってIDEのremoteめsettingをsyncの方法を記載します。

ツール

  • Settings Repository (Browse Jetbrains Plugin)
  • GithubのAccessToken (Settings -> Developer Applications -> Personal access token)

の2点を利用します。

手順

Settings Repository をDLしてきます。
Plugin -> Browse Jetbrains Plugin から Settings Repository をinstallします。

f:id:ema_hiro:20171125031928p:plain

次にGithubのAccessTokenを取得します。
個人のGithubのアカウントを作成しておき
* Settings -> Developer applications -> Personal access token に遷移してGoLand用にアクセストークンを取得します。

GoLandに戻ってきたら Settings Repository を起動します。
GithubリポジトリのURLとaccess token の入力を求められるので、上記手順で取得したgithubaccess tokenを入力します。

適当なプロジェクト開いて
「File」 -> 「Settings Repository」 に遷移します。

f:id:ema_hiro:20171125032249p:plain

この時に以下のようなwindowが表示されるので Override local でローカルを上書きします。

f:id:ema_hiro:20171125032420p:plain

※ どうも一度目は Override local をしなければならないみたいです。

あとはaccess tokenによってremoteで接続すべきGithubのレポジトリも繋がっているので、IDEを終了したタイミングでgithub上に作成したremoteのリポジトリと設定がsyncされます。
これで他端末でも同じ設定が使えます。

refs

Sharing Your IDE Settings - Help | IntelliJ IDEA

qiita.com

QiitaのAPIで遊ぶ

サマリ

APIで遊びながらgoの学習をするシリーズ第二弾で、Qiitaで記事を検索するクライアント goota をgoで書きました。

コードはこちら

github.com

demo

f:id:ema_hiro:20171124023348g:plain

ざっくり仕様

Requirement

  • tagを指定できる。
  • tagはカンマ区切りでOR条件で検索出来る。
  • ストック数が100以上の記事にする。
  • 簡易的なAjaxを使ったSPAとする

はまったところ

Qiitaの仕様が変わって、従来のStocksがQIitaでは「いいね」を指していたので、最初クエリを組み立てる時に likes_count を設定してやろうとしても一件も記事が返ってこなくて困ってた。

refs

qiita API Document

qiita.com

「Qiita APIで投稿一覧を取得するときに、検索クエリをORでつなぐ時の注意点」

qiita.com

githubのSearchAPIで遊んだ話

githubのsearchAPIを簡単にラップしたGUI作りました。

f:id:ema_hiro:20171119031801g:plain

久しぶりにjqueryとか触ったらすごい懐かしい匂いがして色々つまりました。
request処理とかしててハマったところがあるので別でエントリでまとめようと思います。

コードは以下 github.com

refs: はまったところは以下でまとめてみた

ema-hiro.hatenablog.com

Goでファイル読み込みを調べた話

サマリ

  • io.Readerの読み込みついて調べた
  • io.ReaderをWrapして文字列置換
  • io.Readerが一括読み込みでなくstream的な動作で順次読み込みされている

io.Readerについて

refs: io - The Go Programming Language

io.ReaderはデフォルトのReadのWrap。
ioパッケージのReaderはinterfaceとしてReadが設定されている。

type Reader interface {
    Read(p []byte) (n int, err error)
}

文字列置換を実装する

文字列置換を実装する前にgoにおけるinterface埋め込みを使った抽象化で文字列置換を実装します。

interface埋め込みによる抽象化

Readerが標準のReadをinterfaceとして持っているので、このReaderを埋め込むことでReadメソッドに独自の処理を加えてWrapします。

※ goは同一interfaceを定義したメソッドをstructに定義することで実装の詳細をstructに生やしたメソッドに移譲することができる。

cf. io.Reader

type Reader interface {
    Read(p []byte) (n int, err error)
}

goによるinterfaceの埋め込み

type WrapRead interface {
  Read([]byte)(int, error)
}

type WrapReader struct {
   wrapper WrapRead
}

func (w *WrapReader) Read(p []byte) (int, error){
  // WrapReaderでのReadの実装の詳細を記載
} 

以下と書いても同義
※ io.Readerはデフォルトの Read([]byte) (int, error) interfaceのラップなのでわざわざinterfaceで定義し直すのは冗長。

type WrapReader struct {
  reader io.Reader
}

func (w *WrapReader) Read(p []byte) (int, error){
  // WrapReaderでのReadの実装の詳細を記載
}

文字列置換メソッドを実装する

type RewriteWriter struct {
    reader io.Reader
}

func (r *RewriteWriter) Read(p []byte) (int, error) {
    buf := make([]byte, len(p))
    n, err := r.reader.Read(buf) 
    if err != nil && err != io.EOF {
        return n, err
    }

    return copy(p, bytes.Replace(buf, []byte("0"), []byte("1"), -1)), nil
}

デフォルトのReaderを RewriteWriter のstructにreaderという変数で扱えるように埋め込んで置くことで、 RewriteWriter に生やしたReadメソッドでデフォルトのReadに実装の詳細を追加することができる。
置換するので、replaceした内容をもともとのbyteにコピーしています。

io.Readerの動作を確認する。

読み込みを確認するために以下のようなコードを書きました。

handler/main.go

func Top(w http.ResponseWriter, r *http.Request) {
    res, err := http.Get(fmt.Sprintf("http://%v:%v/data", host, port))
    if err != nil {
        fmt.Printf("request get error. err: %v", err)
    }
    body := res.Body
    defer body.Close()
  io.Copy(w, &RewriteWriter{body})
}

func Data(w http.ResponseWriter, r *http.Request) {
    var str string
    for i := 0; i < 10000; i++ {
        str = str + fmt.Sprintf("%v\n", "000")
    }

    w.Write([]byte(str))
}

10000行の文字列を置換するというものです。

動かしてみます。

$ go run main.go
# serverが起動

$ curl http://localhost:8080
111
111
111
111
# 以下同様

10000行程度ならすぐに完了してしまいますが、Readメソッドは読み込むデータを一時的に保存しておくbufferの頭から順々に読み込んでいくような動作をしているようです。
全てのデータを終端記号まで読み込んでから全データを処理するわけではないみたいです。
※ 全てのデータを終端記号まで一括で読み込む場合でから使う場合には ioutil.ReadAllメソッドを使います。

このあたりは 「Goならわかるシステムプログラミング」 の io.Readerの章を確認しながら理解しました。

今回書いたコードは以下

github.com

GAE/GOのversionを上げたらContextが違ってコードが動かなくなってた話

有名な話です。が、いざ自分が体験したので備忘録としてまとめます。

github.com

上記で上げられている netcontextとcontext周りで死ぬ というのに引っかかりました。

課題

go1.6上で以下のようなリクエスト比較するコードを書いてました。 ※ コードはあくまでサンプルです。

url := "https://www.gooogle.com"
values := url.Values{}
req_1, _ := http.NewRequest("POST", url, strings.NewReader(values.Encode()))
req_2, _ := http.NewRequest("POST", url, strings.NewReader(values.Encode()))

if reflect.DeepEqual(req_1, req_2) {
  // requestが同値である。
} else {
  // requestは同値でない
}

req_1req_2 をdeepEqualで比較して、同値性を判別したいという意図でしたが、このコードはgo1.6だと同値だと判定されますが、go1.8だと同値だと判定されません。

理由は最初に書いた netcontextとcontext周りで死ぬ が原因だと思われます。

goのx/net/context パッケージが標準の context に入ったというのは有名です。
refs: Go 1.7 Release Notes - The Go Programming Language

net/context も標準のcontextとして扱われるようになったので、req_1req_2 は別々のリクエスト、すなわち異なるcontextを持っていると判定され、それを reflect.DeepEqual にかけた場合、標準の context 違いがあるので、同値判定されません。

go1.7のcontextについては以下のブログがすごく勉強になりました。

Go1.7のcontextパッケージ | SOTA

同値性の比較

では、requestの比較をしたい場合はどうすればいいかというと、 httputil.DumpRequest を使います。 refs: httputil - The Go Programming Language

url := "https://www.gooogle.com"
values := url.Values{}
req_1, _ := http.NewRequest("POST", url, strings.NewReader(values.Encode()))
req_2, _ := http.NewRequest("POST", url, strings.NewReader(values.Encode()))

// dumpは[]byte型
dump_1, _ := httputil.DumpRequest(req_1, true)
dump_2, _ := httputil.DumpRequest(req_2, true) 

// DeepEqual
if reflect.DeepEqual(dump_1, dump_2){
  // requestの同値判定
}

// bytes.Equal
if bytes.Equal(dump_1, dump_2) {
  // requestの同値判定
}

// stringに変換して文字列比較
if string(dump_1) == string(dump_2) {
  // requestの同値判定
}

DumpRequest することで context ではなくリクエストそのものを比較出来ます。
比較方法は、以前同様 DeepEqual を使ってもいいですし、 []byte 型に変換されるので、それに合わせて bytes packageを使ってもいいですし、文字列に変換して文字列一致をしても同値性を取ることが出来ると思います。

time.IsZero()の挙動でハマった話

サマリ

  • goのtimeパッケージの IsZero() はUnixTime = 0ではない
  • GAEのdatasotreのdefaultの時刻で IsZero() を使ってもtrueを返さない

IsZero()メソッドについて

refs: time package - time - pkg.go.dev

IsZero reports whether t represents the zero time instant, January 1, year 1, 00:00:00 UTC. 

IsZero()は 01-01 00:00:00 UTC の時にtrueを返します。
ここで注意するべきはtrueを返す時刻はunixtimeのスタート時刻 1970-01-01 00:00:00 +0000 UTC を指し示すわけではないということでです。

実際の挙動を見てみます。

def := time.Time{}
fmt.Printf("%v\n", def)
fmt.Printf("%v\n", def.IsZero())
// output
// 0001-01-01 00:00:00 +0000 UTC
// true

time.Time{} は何もない時刻をinstance化する、すなわちtimeパッケージにおける標準時刻をinstance化することですが、これの結果は 0001-01-01 00:00:00 +0000 UTC という時刻がinstance化され、IsZero() はこの時刻のときのみtrueを返します。

udef, _ := time.Parse("2006-01-02 15:04:05 -0700", "1970-01-01 00:00:00 +0000")
fmt.Printf("%v\n", udef)
fmt.Printf("%v\n", udef.IsZero())
// output
// 1970-01-01 00:00:00 +0000 +0000
// false

一方でコンピューターにおける時刻ゼロとはunixtimeの1番最初だと想起できるので、unixtimeのスタートした時刻に対して IsZero() をcallすると、unixtimeのstartの時刻にもかかわらず false を返します。

timeパッケージの中身を見てみると

type Time struct {
    // sec gives the number of seconds elapsed since
    // January 1, year 1 00:00:00 UTC.
    sec int64

    // nsec specifies a non-negative nanosecond
    // offset within the second named by Seconds.
    // It must be in the range [0, 999999999].
    nsec int32

    // loc specifies the Location that should be used to
    // determine the minute, hour, month, day, and year
    // that correspond to this Time.
    // Only the zero Time has a nil Location.
    // In that case it is interpreted to mean UTC.
    loc *Location
}

とあり、そもそもの sec = 0 の時には January 1, year 1 00:00:00 UTC. が初期値設定されています。
IsZero() については

// IsZero reports whether t represents the zero time instant,
// January 1, year 1, 00:00:00 UTC.
func (t Time) IsZero() bool {
    return t.sec == 0 && t.nsec == 0
}

とあるので、そもそもunixtime=0を返さないのはgoのtimeパッケージの仕様のようです。

GAE上での挙動について

さて、ここで困ったのがGAEでDatastore上に time.Time 型で標準時刻をinstance化した時のことです。

以下のようなstructを考えてみます。

type App struct {
  ID         int       `datastore: "ID"         json: "id"`
  CreatedAt  time.Time `datastore: "createdAt"  json: "created_at"`
  UpdatedAt  time.Time `datastore: "updatedAt"  json: "updated_at"`
  ReleasedAt time.Time `datastore: "ReleasedAt" json: "released_at"`
}

この App Entityがcreateされた時に CreatedAtUpdatedAt はそれぞれcreateされた時刻が入りますが リリースされたわけではないので、 ReleasedAt には何も入りません。
つまり、 ReleasedAt のfieldには time.Time{} が入ってくることを期待してました。
しかし実際には 1970-01-01 00:00:00 +0900 JST という日本標準時のでのunixtime = 0の状態が入っていました。

つまり、 ReleasedAt に一度しか値を入れたくない、みたいな要件があったときに

if !app.ReleasedAt.UTC().IsZero() {
    // ReleasedAtにすでに値が入っている時
} else {
    // ReleasedAtに初回に値が入る    
}

上記のような条件分岐を考慮した場合、どんなときでも else 以下に入ってしまいます。
理由は上記で述べた通り、 unixtime のスタートはtimeパッケージで IsZero 判別するときには false を返してしまうからです。

ではどうすればいいかというと、実は unixtimeの最初の状態を作り出した time オブジェクトのunixtimeを取ると 0 になります。

udef, _ := time.Parse("2006-01-02 15:04:05 -0700", "1970-01-01 00:00:00 +0000")
fmt.Printf("%v\n", udef)
fmt.Printf("%v\n", udef.UTC().Unix())
// output
// 1970-01-01 00:00:00 +0000 +0000
// 0

これを利用して上記の条件分岐を以下のように書き換えます。

if app.ReleasedAt.UTC().Unix() != 0 {
    // ReleasedAtにすでに値が入っている時
} else {
    // ReleasedAtに初回に値が入る    
}

app.ReleasedAt.UTC().Unix() とすることで、すでに ReleasedAt に値が入ってきている場合は、 Unix() でunixtimeに変換した時に 0以外 が入ってくる事になります。

まとめ

timeパッケージにおける IsZero() の挙動とGAEのDatastoreでデフォルトの時刻を unixtime = 0 判定を同様に考えてきて、かなりハマりました。
IsZero() がunixtimeのstart時刻を示さないのはどうにも納得が行きませんが、timeパッケージ的にはどうしようもなさそうなので、注意しようと思いました。

追記

このエントリを書いてから 4年以上経って言及されるとは思ってなかったですが、 いい感じに答えたが書いてあって参考にしたいなと思いました。

【続】FWに頼らないオレオレroutingを実装する

前回書いた記事の中でオレオレroutingを実装する際に標準の net/http パッケージだけだと足りないと書いてましたがこれ、間違いでした。

ema-hiro.hatenablog.com

標準の net/http パッケージだけでオレオレroutingを実装する方法は以下

main.go

package main

import (
    "gothub/handler"

    "fmt"
    "net/http"

    "github.com/labstack/gommon/log"
)

const port = "8080"

func main() {
    router := http.NewServeMux()
    router.HandleFunc("/", handler.Top)
    if err := http.ListenAndServe(fmt.Sprintf(":%s", port), router); err != nil {
        log.Fatal("err: %v", err)
    }
}

handler/handler.go

pakage handler

import (
    "net/http"
)

func Top(w http.ResponseWriter, r *httpRequest){
    // serverの処理
}

router := http.NewServeMux()HTTP Request multiplexerインスタンス化し、routerとして扱う。
http requestをhandleしたいroutingのメソッドには http.ResponseWriterhttp.Request を引数に与える。

routingのライブラリを使うことなく、標準のHTTPパッケージだけでもやりたかった、超薄いAPIを作るということは可能でした。

(golangのhttpパッケージすげぇ強力だなぁ(小並感))

GogLandでtmplファイルをhtmlのシンタックス対象に加える

いつからはわからないですが、GogLand EAPをアップデートしたらtmplファイルがhtmlのシンタックス対象から外れてて、htmlを開いてもxmlと判別されてエラーがうるさくなってしまったので、カスタムファイルとしてtmplファイルのときは、htmlのシンタックスを追加する方法を記載します。

手順

Preferences -> Editor -> FileType でhtmlを選択し、シンタックス適用の登録ファイルにtmplを追加する。

画像は以下の場所 (+) マークを押下して、 tmpl ファイルを追加します。

f:id:ema_hiro:20171107012953p:plain