emahiro's ProgrammingBlog

勉強記録と書評とたまに長めの呟きを書きます

【go】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のAPIで遊んだ話 その1

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

f:id:ema_hiro:20171119031801g:plain

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

コードは以下 github.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 - The Go Programming Language

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パッケージ的にはどうしようもなさそうなので、注意しようと思いました。

【続】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