17
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Go の 'net/http' でリクエストを投げるまでの足跡

TL;DR

Go の標準ライブラリ net/http で http リクエストを実装してみた。

Snippet は これ

ポイントとしては、レスポンスを print して「データ取れたね!」じゃなくて、実際に使う事を意識したコードにしてみたこと。
標準ライブラリの使い方は色んな所にあるんだけど、レスポンスの内容を print して「リクエストを取れてるのが分かったね」っていう物がほとんどで、「結局実際使う時はどう実装するんだろう?」という疑問に答えてくれる記事が見当たらなかった。

  • 学んだこと
    • Response の Body を渡すスタンダードな方法
    • Cookie を含むリクエストの方法
  • まだ勉強できていないとこ
    • oauth パッケージの使い方
    • context パッケージの使いどこ

参考URL

今回参考に見たのは次のパッケージたち

ちなみにこの記事を書いている最中に肝となるポイントをおさえた記事を発見してしまった。
https://deeeet.com/writing/2016/11/01/go-api-client/

めげずに書ききる!←

というわけで足跡をたどる

今回自分がしたかったのは、「社内で使われいる node で書かれた既存の CLI ツールを Go で書き直す」事。
ちなみに個人の学習のための教材として作っているだけで書き直しのオーダーが入っていたわけではない。

とりあえずサンプルをなぞる

まず、何はともあれ Go で http 通信をしたいので "golang http" とかで調べるとまぁもちろん Go の標準パッケージ net/http のドキュメントページが表示されるわけで、まずはドキュメントの確認から。Overview を見ると http.Get とか http.Post みたいな関数が用意されているとのこと。
ただ、API を叩く事を考えると、もっと柔軟にカスタマイズしたい訳で、さらに読み進めるとそのためには http.Client を生成して、それに対して http.Request というデータを渡す必要があるらしい。

という事でまずは http.Client を用意する

// https://golang.org/pkg/net/http/#pkg-overview
// simple GET request; dump all response to stdout
func main() {
    client := &http.Client{}
    req, err := http.NewRequest("GET", "https://example.com", nil)
    if err != nil {
        // handle error
    }
    resp, err := client.Do(req)
    if err != nil {
        // handle error
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        // handle error
    }

    fmt.Println(body)
}

これが公式ドキュメントを元に作る一番シンプルなコード。

まずは関数にまとめる

とりあえず使いまわせるようするために関数にまとめてみた。

ちなみに調べている中でGo のデフォルトの http クライアントは Timeout が設定されなくて死ぬという記事を見かけた。
今回作ろうとしていたのは CLI ツールだったから、そこまで大きな問題にはならないもののこれに習って Timeout を設定しておく。

func request(method, path string, body io.Reader) (*http.Response, error) {
    client := &http.Client{
        Timeout: 30 * time.Second,
    }

    req, err := http.NewRequest(method, path, body)
    if err != nil {
        return nil, err
    }

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    fmt.Println(data)
    return resp, nil
}

レスポンスはどうやって返すべきか

ここで気になるのはレスポンスのデータをどう返すか。
結論としては「データを受け取りたい変数を渡して、それに入れてもらう」のが今のところの解答。

func request(method, path string, body io.Reader, data interface{}) (*http.Response, error)

HTTP Status だったり Header を確認する事を考えるとこの関数が *http.Response を返すのはそのままで良さそう。ただ、ネックなのが resp.Body に関しては Close() を呼ぶ必要があること。これをあまりリクエストした点から離すと簡単に Close を忘れてしまいそうなので、defer を使って予め閉じてしまいたい。すると今度は呼び出し側から resp.Body を参照しても nil になってしまう。

一番簡単なのは次の定義みたいに ioutil.ReadAll で得た内容を返り値として使うこと。

func request(method, path string, body io.Reader) (*http.Response, []byte, error)

ただ、 ioutil.ReadAll といったサンプルコードに載っている関数は実際の開発では避ける印象が強いので、実際のところ他のライブラリはどうしてるのかを見に行った。

リクエストしたタイミングで defer を定義している最初の2つのライブラリはどちらも interface{} 型の変数を渡して、そこにパースした内容を突っ込む手法を取っていたので、自分のコードもその手法を利用する事に。
特に go-github は生のデータを取得する方法も提供していたので、今回はそれをそのまま参考にした。

func request(method, path string, body io.Reader, v interface{}) (*http.Response, error) {
    // ...略...

    if v != nil {
        if w, ok := v.(io.Writer); ok {
            // give *bytes.Buffer to get raw bytes instead of json decoded string
            io.Copy(w, resp.Body)
        } else {
            err = json.NewDecoder(resp.Body).Decode(v)

            // ignore the error caused by an empty response
            if err == io.EOF {
                err = nil
            }
        }
    }

    return resp, nil
}

最初は「なぜ返り値を使わずに引数で渡す手法にしたのか?」という疑問もあったけど、よくよく考えると interface{} で返されても呼び出し側としては何が返ってきたか分からないという問題があるのかと納得。「こういう形でデータがほしい」と明示的に呼べるのは強みだなと。

Cookie も使いたい

外部向けの API サーバーなら oauth とかを設定しているところなのだけど、今回はそこまで API が整理されていない状態のため、Cookie を用いたセッションで認証をパスする必要がある。

そこで調べたところ CookieJar という仕組みを発見。これを Client に持たせれば cookie を使いながらリクエストができるとのこと。
ただ、今の実装だとリクエストの度に Client を作り直しているので意味がない。のでClient 生成を切り離して、request 関数に渡せるようにした。

func main() {
    client, err := newClient()
    // ...略...

    buf := new(bytes.Buffer)
    resp, err := request(client, "GET", "https://example.com", nil, buf)
    // ...略...
}

func newClient() (*http.Client, error) {
    jar, err := cookiejar.New(&cookiejar.Options{
        PublicSuffixList: publicsuffix.List,
    })
    if err != nil {
        return nil, err
    }

    client := &http.Client{
        Jar:     jar,
        Timeout: 30 * time.Second,
    }
    return client, nil
}

func request(client *http.Client, method, path string, body io.Reader, v interface{}) (*http.Response, error) {
    // ...略...
}

url.Values

リクエストを作成するときに io.Reader だと使い勝手が悪いから、何かうまい具合に io.Reader に変換してくれるもの無いかなぁと探していたら url.Values という構造体があることを発見。

使い方は次の通りでとてもシンプル。

values := url.Values{}
values.Set("key", "value")
values.Set("other", "value")
println(values.Encode()) // key=value&other=value

ついでに request の関数が長くなってきたので http.Request の生成も切り離すことに。

func main() {
    client, err := newClient()
    // ...略...

    req, err := newRequest("GET", "https://example.com", url.Values{})
    // ...略...

    buf := new(bytes.Buffer)
    resp, err := do(client, req, buf)
    // ...略...
}

func newRequest(method, path string, values url.Values) (*http.Request, error) {
    body := strings.NewReader(values.Encode())
    req, err := http.NewRequest(method, path, body)
    if err != nil {
        return nil, err
    }
    return req, nil
}

func do(client *http.Client, req *http.Request, v interface{}) (*http.Response, error) {
    // ...略...
}

終わりに

というわけで、完成形が上にも載せたけど これ

まぁこれから更に struct に入れるとか Context 使うとかする必要はあるんだろうけど、まだ整理ができていないのでこの記事はここまで :P

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
17
Help us understand the problem. What are the problem?