Golangでヘッダとボディを指定してHTTPリクエストを投げる

GolangでHTTPリクエストを投げたいときに、http.Get()とかhttp.Post()ばっかり使っていて、Headerに認証情報を持たせてリクエストを投げたいときに、投げ方が分からなかったのでその話。

http.Post()は何をしているのか

http.Post()はURLとボディの他にContent-Typeも引数に渡しています。Content-TypeはHeaderに載せる情報なので、http.Post()がどうやってリクエストを送っているのかソースコードを追えばHeaderとBodyを指定してリクエストを投げる方法が分かりそうなので追ってみます。

func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
return DefaultClient.Post(url, contentType, body)
}

DefaultClientは、空のhttp.Clientです。

// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}

レシーバの型がhttp.Clientであることが分かったので、(*http.Client) Post()の方も追っていきます。

func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
req, err := NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.Do(req)
}

お、ついにHeaderをセットしているらしき部分が出てきました。

どうもこの関数を参考にすればいけそうです。

 

自分でHTTPリクエストを組み立ててみる

(*http.Client) Post()を参考にHTTPリクエストを組み立ててみます。

func NewRequest(method, url string, header map[string]string, body []byte) (*http.Request, error) {
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
for key, value := range header {
req.Header.Set(key, value)
}
return req, nil
}

これでmapで与えられたHeaderを全てセットしたHTTPリクエストを作ることができました。 

 

HTTPリクエストの送信

正しくHeaderとBodyが指定されてHTTPリクエストが送信されているか確認するために、簡単なサーバを書きました。

func Server() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
for key, value := range r.Header {
fmt.Fprintln(w, key, value)
}
io.Copy(w, r.Body)
})
http.ListenAndServe("", nil)
}

http://localhost/へのリクエストに対して、HeaderとBodyが列挙された文字列をレスポンスBodyに詰めて返すだけのサーバです。r.BodyをClose()していなかったり、errorを無視していたり、お行儀の悪いコードですけど、今はそういうことはどうでも良いので割愛しています。

ここにリクエストを投げて、レスポンスを確認してみます。

package main

import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"time"
)

func Server() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
for key, value := range r.Header {
fmt.Fprintln(w, key, value)
}
io.Copy(w, r.Body)
})
http.ListenAndServe("", nil)
}

func NewRequest(method, url string, header map[string]string, body []byte) (*http.Request, error) {
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
for key, value := range header {
req.Header.Set(key, value)
}
return req, nil
}

func Client() {
var (
method = "POST"
url = "http://localhost"
header = map[string]string{
"Content-Type": "application/json",
"Authorization": "key=********",
}
body = []byte(`{"id"}:"1234567890"`)
)
req, err := NewRequest(method, url, header, body)
if err != nil {
fmt.Println(err.Error())
}
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println(err.Error())
}
io.Copy(os.Stdout, res.Body)
}

func main() {
go Server()
go Client()
time.Sleep(time.Second)
}

time.Sleep()で1秒ぐらい待てばレスポンスが返ってくると思うので、これで実行してみます。

$ go run main.go
User-Agent [Go-http-client/1.1]
Content-Length [19]
Authorization [key=********]
Content-Type [application/json]
Accept-Encoding [gzip]
{"id"}:"1234567890"

ちゃんとHeaderもBodyもセットされています。やりたいこと達成できたねやったね。

この記事だと、net/httpの実装を追ってるのでリクエストの送信にhttp.DefaultClientを使ってるんですけど、適当にリクエストの投げ方をネットで検索すると、

client := &http.Client{}
res, err := client.Do(req)

よくこんな感じでDo()を呼ぶためだけに空のhttp.Clientを生成してリクエストを投げている記事を見かけますが、特に使いもしないのに空のClientを生成するのは気持ち悪いしメモリ効率も悪い(まあ構造体1つ生成するだけなので影響なんて超超超小さいけど)のでやめようね。

 

まとめ

まとめるとhttp.NewRequest()してreq.Header.Set(key, value)してhttp.DefaultClient.Do(req)すればいいです。

今回はあんまりググらずにコードリーディングでなんとかしたけど、Golandの関数ジャンプ機能便利で良いですね。

まあ関数ジャンプなんて大体のIDEには普通に標準で付いてる機能ではあるんですが、

今までずっとEmacsを使っていて、tagsで関数ジャンプはできるものの、プロジェクト外のソースコードにまではジャンプできなかったので、もどかしさを感じていたら、ここにユーザが本当に求めていたものがあった。