Go言語(golang) HTTPリクエスト
Go言語のnet/httpパッケージは Transport,Client,Request,Response に分けて考えたほうがいいので、
それらの役割を確認しながら使い方を説明してます。
GitHubを探せば gorequest、resty、sling などHTTPクライアント系パッケージがあるので、
コードを読んで参考にしながら、一度自分でパッケージを作ってみるとより理解が深まるのでおすすめです。
目次
基本的な使い方
大まかな使い方は httpのOverview に載っています。
それぞれの定義
以下は、http.NewRequest
を使ったリクエスト送信です。
package main import ( "fmt" "io/ioutil" "log" "net/http" ) func main() { // 第一引数: http.MethodGet は "GET" を指定するのと同じ // 第二引数: リクエストするURL // 第三引数: リクエストボディで フォーム、ファイル などのデータを送信する場合に使う。何も送信しない場合は nil を指定する req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) if err != nil { log.Fatal(err) } // リクエストの送信 resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } // ステータスコード resp.StatusCode // ヘッダーを取得 resp.Header.Get("Content-Type") // resp.Bodyの大きさ len(body) と同じ resp.ContentLength // リクエストURL resp.Request.URL.String() // レスポンスボディをすべて読み出す body, _ := ioutil.ReadAll(resp.Body) // body は []byte fmt.Println(string(body)) }
Transport - トランスポート
http.DefaultTransportは以下のようになってます。
使用するときには、RoundTripper であることに注意
var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }
各ホストに対するKeep-Aliveの数
TransportのMaxIdleConnsPerHostに変更がない場合(値が0の場合)に、このデフォルト値が使われる
const DefaultMaxIdleConnsPerHost = 2
TLSを有効にする
package main import ( "crypto/tls" "log" "net/http" ) func main() { req, err := http.NewRequest(http.MethodGet, "https://example.com", nil) if err != nil { log.Fatal(err) } // 証明書を検証せずにリクエストを送る場合これを設定する http.DefaultClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, } _, err = http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } }
Proxy - プロキシ
デフォルトは ProxyFromEnvironment になっているので
環境変数に HTTP_PROXY, HTTPS_PROXY, NO_PROXY が設定してあった場合はそれが使われる。
// http.DefaultTransport は http.RoundTripper なので http.Transport に戻す transport := http.DefaultTransport.(*http.Transport) // http.Transport の Proxy にセットする u, _ := url.Parse("proxy address") transport.Proxy = http.ProxyURL(u) client := &http.Client{ Transport: transport, }
Sock5 Proxy - ソックス 5 プロキシ
package main import ( "context" "fmt" "log" "net" "net/http" "golang.org/x/net/proxy" ) type dialer struct { addr string socks5 proxy.Dialer auth *proxy.Auth } // 引数にコンテキストを受け取る func (d *dialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { return d.Dial(network, addr) } func (d *dialer) Dial(network, addr string) (net.Conn, error) { var err error if d.socks5 == nil { d.socks5, err = proxy.SOCKS5("tcp", d.addr, d.auth, proxy.Direct) if err != nil { return nil, err } } return d.socks5.Dial(network, addr) } func main() { d := dialer{ addr: "localhost:9050", // 認証が必要ならユーザー名とパスワードとセットする // auth: &proxy.Auth{ // User: "username", // Password: "password", // }, } // http.DefaultTransport は http.RoundTripper なので http.Transport に戻す t := http.DefaultTransport.(*http.Transport) // dialerをセットする t.DialContext = (&d).DialContext http.DefaultClient.Transport = t req, err := http.NewRequest(http.MethodGet, "https://example.com", nil) if err != nil { log.Fatal(err) } _, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } }
RoundTripper - ラウンドトリッパー
RoundTripperは http.Transport に RoundTrip 関数を実装したインターフェイスのことなんですが、
リクエストの送信前とレスポンスを受け取った後に(ロギングやキャッシュなど)何らかの処理を付け加えるときに独自で作成します。
以下のように定義されているので、RoundTrip関数を実装します。
type RoundTripper interface { RoundTrip(*Request) (*Response, error) }
以下、簡単なRoundTrip実装。
package main import ( "fmt" "log" "net/http" "net/http/httputil" ) type CustomTransport struct { Transport http.RoundTripper } // 念の為 CustomTransoprt.Transport が nil だったら http.DefaultTransport を返すようにする func (c *CustomTransport) transport() http.RoundTripper { if c.Transport == nil { return http.DefaultTransport } return c.Transport } // RoundTrip の実装 func (c *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { // リクエストの内容を表示 b, err := httputil.DumpRequest(req, false) if err != nil { return nil, err } log.Println(string(b)) // ここでリクエストが送信される。本当の http.Transport の RoundTrip を呼び出す resp, err := c.transport().RoundTrip(req) if err != nil { return nil, err } // レスポンスの内容を表示 b, err = httputil.DumpResponse(resp, false) if err != nil { return nil, err } log.Println(string(b)) return resp, err } func main() { // 実装しておけば、そのまま簡素に書ける c := &http.Client{ Transport: &CustomTransport{Transport: http.DefaultTransport.(*http.Transport)}, } req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) if err != nil { log.Fatal(err) } _, err := c.Do(req) if err != nil { log.Fatal(err) } }
Client - クライント
http.Clientは以下のように定義されてます。
type Client struct { Transport RoundTripper CheckRedirect func(req *Request, via []*Request) error Jar CookieJar Timeout time.Duration }
デフォルトクライントは以下のようになっていてるので、DefaultTransportが使われることになる
var DefaultClient = &Client{}
http.Clientは1つを使う回す必要がある。
複数回リクエストを送る場合は、広いスコープに new しておくか、構造体にでも入れておく
package main import ( "log" "net/http" ) func main() { urls := []string{ "http://example.com/1", "http://example.com/2", "http://example.com/3", } client := new(http.Client) for _, v := range urls { // リクエストごとにクライントを new するのはダメ // client := new(http.Client) req, err := http.NewRequest(http.MethodGet, v, nil) if err != nil { log.Fatal(err) } _, err = client.Do(req) if err != nil { log.Fatal(err) } } }
Timeout - タイムアウト
- クライアントのTimeoutに秒数を設定する。0の場合は、タイムアウトしない
http.DefaultClient.Timeout = 120 * time.Second // タイムアウトしたら、以下のようなエラーになる // net/http: request canceled (Client.Timeout exceeded while awaiting headers)
- コンテキストを使ったタイムアウト
package main import ( "context" "log" "net/http" "time" ) func main() { req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() req = req.WithContext(ctx) _, err := http.DefaultClient.Do(req) if err != nil { // タイムアウトしたら、以下のエラーになる log.Fatal(err) // context deadline exceeded } }
Redirect - リダイレクト
http.Client
の CheckRedirect
にリダイレクトポリシー関数をセットする。
回数制限以外にもリクエストの内容に問題があれば、エラーにして止めることができる
package main import ( "errors" "net/http" ) func RedirectPolicyFunc(n int) func(*http.Request, []*http.Request) error { return func(req *http.Request, via []*http.Request) error { // req には、次のリクエストが入っている。 // via にはリクエストが追加されてくので、その数で制限を設ける if len(via) >= n { return errors.New("Redirection limit exceeded") } return nil } } func main() { // FireFoxのリダイレクト(network.http.redirection-limit)は 20 http.DefaultClient.CheckRedirect = RedirectPolicyFunc(20) }
- リダイレクトのURL
// 最後のリクエストURL。リダイレクトの有無にかかわらずURLはここにある resp.Request.URL.String() // 最後から2番目のリクエストURL resp.Request.Response.Request.URL.String() // レスポンスのリクエストを辿っていくことで、リダイレクト元のURLをすべて知ることができる resp.Request.Response.Request.Response.Request...
- リダイレクトURLをすべて取り出す
// 試しはしたけど、多分こんなんで問題ないはず。 var a []string r := resp for i := 0; r != nil; i++ { rv := reflect.ValueOf(r).Elem() vv := rv.FieldByName("Request") rp, ok := vv.Interface().(*http.Request) if !ok { break } a = append(a, rp.URL.String()) r = rp.Response }
CookieJar - クッキージャー
cookiejar はレスポンスにクッキーがあれば保持して、次回以降のリクエストにクッキーを付加するやつです。
$ go get golang.org/x/net/publicsuffix
package main import ( "fmt" "log" "net/http" "net/http/cookiejar" "net/url" "golang.org/x/net/publicsuffix" ) func main() { jar, err := cookiejar.New(&cookiejar.Options{ PublicSuffixList: publicsuffix.List, }) if err != nil { log.Fatal(err) } // jar をセットする http.DefaultClient.Jar = jar // 自動的にクッキーを使ってくれる req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) _, err = http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } // 追加と取得は以下でできる cookies := []*http.Cookie{ { Name: "name", Value: "value", Path: "/", }, } // *url.URL でクッキーをセットする u, _ := url.Parse("https://example.com") http.DefaultClient.Jar.SetCookies(u, cookies) // URLのクッキーを取得 cookie := http.DefaultClient.Jar.Cookies(u) fmt.Println(cookie) // [name=value] // 当然、違うドメインでは取得できない u, _ = url.Parse("https://www.example.com") cookie = http.DefaultClient.Jar.Cookies(u) fmt.Println(cookie) // [] }
Request - リクエスト
Requestは http.Request に定義されています。
Header - ヘッダー
package main import ( "fmt" "log" "net/http" "net/http/httputil" ) func main() { req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) if err != nil { log.Fatal(err) } // Set か Add でヘッダーをセットする。削除する場合は Del を使う // 内部的に CanonicalMIMEHeaderKey が使われるので、accept のように小文字でも問題ない req.Header.Set("Accept", "text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8") b, err := httputil.DumpRequest(req, false) if err != nil { log.Fatal(err) } fmt.Println(string(b)) // GET / HTTP/1.1 // Host: example.com // Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8 }
Query - クエリ
最初からNewRequestにクエリ付きのURLを渡せばいいけど、追加するなどの場合は、
req.URL.Query()
で url.Values
にしてから追加して再度戻す
package main import ( "fmt" "log" "net/http" ) func main() { req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) if err != nil { log.Fatal(err) } // url.Values にする。 values := req.URL.Query() values.Set("key", "value") // 戻す req.URL.RawQuery = values.Encode() fmt.Println(req.URL.String()) // http://example.com?key=value }
Form - フォーム
http.PostForm
を使ってもできるけど、NewRequest
を使ったフォーム送信。
package main import ( "log" "net/http" "net/url" "strings" ) func main() { // url.Values は、内部的には map なので、make する必要がある values := make(url.Values) values.Set("key", "value") // POSTメソッドにする req, err := http.NewRequest( http.MethodPost, "http://example.com", strings.NewReader(values.Encode())) if err != nil { log.Fatal(err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") _, err = http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } }
BasicAuth - ベーシック認証
package main import ( "fmt" "log" "net/http" "net/http/httputil" ) func main() { req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) if err != nil { log.Fatal(err) } req.SetBasicAuth("username", "password") b, err := httputil.DumpRequest(req, false) if err != nil { log.Fatal(err) } fmt.Println(string(b)) // GET / HTTP/1.1 // Host: example.com // Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= }
UTF8にエンコード
go-encodingパッケージを使わせてもらって、この記事を参考にさせてもらう
$ go get https://github.com/mattn/go-encoding
テストした限りでは、Shift-JISからUTF-8に変換できました。
package main import ( "bufio" "fmt" "io" "io/ioutil" "log" "net/http" "github.com/mattn/go-encoding" "golang.org/x/net/html/charset" ) func ToUTF8FromReader(reader io.Reader, contentType string) io.Reader { br := bufio.NewReader(reader) var r io.Reader = br if data, err := br.Peek(4096); err == nil { enc, name, _ := charset.DetermineEncoding(data, contentType) if enc != nil { r = enc.NewDecoder().Reader(br) } else if name != "" { if enc := encoding.GetEncoding(name); enc != nil { r = enc.NewDecoder().Reader(br) } } } return r } func main() { req, err := http.NewRequest(http.MethodGet, "https://example.com", nil) if err != nil { log.Fatal(err) } resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } body, _ := ioutil.ReadAll(ToUTF8FromReader(resp.Body, resp.Header.Get("Content-Type"))) fmt.Println(string(body)) }