こんにちは。Go入門ブログの第26回です。
今回も引き続き、GoにおけるWEB開発の基礎となる知識を解説していきたいと思います。
以下の記事では、net/httpパッケージを使用したWEBサーバの構築方法と、net/urlパッケージを使用したURLのパース・生成方法を解説しました。
【Go入門】net/httpパッケージを使ったWEBサーバの構築とHTTPリクエストハンドラの実装
【Go入門】net/urlパッケージによるURLのパースと生成
今回からは2回に渡り、HTTPクライアントの実装、つまりHTTPリクエストの送信処理方法を解説していきます。
今回の記事では、主にGETリクエストの送信方法を解説します。
解説に当たりnet/urlパッケージの機能を使用する箇所がありますので、必要に応じて上記リンク先の記事を参照してください。
Contents
GoにおけるHTTPクライアント処理の実装方法
GoにおけるHTTPクライアントの実装には、サーバの実装と同様にnet/httpを使用できます。
HTTPリクエストの送信方法は、GET/POSTともに、大きく以下の2通りの方法に分けられます。
- net/httpパッケージに定義されている関数を使用する方法
- net/httpパッケージに定義されているClient構造体に実装されたメソッドを使用する方法
具体的な送信方法をみていく前に、http.Client構造体について簡単に解説します。
http.Client構造体
http.Client構造体は、その名の通りHTTPクライアントを表します。
net/httpパッケージに以下のように定義されています。
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration
}
Transportプロパティは、名前の通りHTTPトランスポートであり、ローレベルのHTTP通信の実態を表します。
file://プロトコルを表すhttp.fileTransportや、oauth2認証を扱うoauth2.Transportなどが指定できます。
nilの場合は、http.DefaultTransport(デフォルトトランスポート)が使用されます。
型はhttp.RoundTripperという独自インターフェイスですが、これについての詳細な解説は割愛します。
CheckRedirectプロパティは、リダイレクトを処理するためのポリシーを指定します。
引数reqとviaを持つ関数型です。
CheckRedirectがnil以外の場合、クライアントはHTTPリダイレクトをたどる前にこの関数を実行します。
Jarプロパティは、クッキージャーを指定します。
Jarは送信リクエストにクッキーを挿入するために使用され、レスポンスのクッキー値で更新されます。
Timeoutプロパティは、このクライアントによるリクエストの制限時間を指定します。
タイムアウトがゼロの場合は、タイムアウトしないことを意味します。
GETリクエストの送信
それでは、具体的なGETリクエストの送信方法をみていきましょう。
http.Get関数を使用する
もっとも簡単なGETリクエストの送信方法は、http.Get関数を使用することです。
http.Get関数は、引数で指定されたURLにGETを発行します。
リダイレクトが多すぎる場合、またはHTTPプロトコルエラーが発生した場合はエラーが返されます。
以下は実装例です。
まずは、リクエストを受け取るサーバーを以下のように記述します。
package main import ( "io" "net/http" ) func hogeHandler(w http.ResponseWriter, req *http.Request) { // HTMLテキストをhttp.ResponseWriterへ書き込む io.WriteString(w, ` <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Go | net/httpパッケージ</title> </head> <body> <h1>Hello, World!</h1> <p>これはnet/httpで実装したlocalhostのWEBサーバです</p> </body> </html> `) } func main() { // localhostへのリクエストをhogeHandler関数で処理する http.HandleFunc("/", hogeHandler) // localhost:8080でサーバー処理開始 http.ListenAndServe(":8080", nil) }
次に、クライアント側のコードを以下のように記述します。
package main import ( "fmt" "io/ioutil" "net/http" "net/url" ) func main() { // URLを生成 u := &url.URL{} u.Scheme = "http" u.Host = "localhost:8080" // url文字列 uStr := u.String() // GETリクエスト発行 rsp, err := http.Get(uStr) if err != nil { fmt.Println(err) return } // 関数を抜ける際に必ずresponseをcloseするようにdeferでcloseを呼ぶ defer rsp.Body.Close() // レスポンスを取得し出力 body, _ := ioutil.ReadAll(rsp.Body) fmt.Println(string(body)) }
上記のserver.goを実行し、サーバを起動した状態で、client.goを実行してください。
コンソールに以下のような出力が得られるはずです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Go | net/httpパッケージ</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>これはnet/httpで実装したlocalhostのWEBサーバです</p>
</body>
</html>
期待通りのHTMLレスポンスが得られていることが確認できますね。
Client構造体のGetメソッドを使用する
続いて、Client構造体に実装されたGetメソッドを解説します。
Client.Getの定義は以下の通りです。
http.Get関数と同様に、引数で指定されたURLにGETを発行します。
というより、前述のhttp.Get関数の実態が、DefaultClient.Getのラッパーです。
したがって使用感はhttp.Get関数とほぼ同じですが、タイムアウト値やクッキー値などを指定したい場合にはこちらを使用することになります。
それでは実装例です。
先ほどのserver.goはそのままに、client.goを以下のように書き換えてみてください。
package main import ( "fmt" "io/ioutil" "net/http" "net/url" "time" ) func main() { // URLを生成 u := &url.URL{} u.Scheme = "http" u.Host = "localhost:8080" // url文字列 uStr := u.String() // タイムアウトを30秒に指定してClient構造体を生成 cli := &http.Client{Timeout: time.Duration(30) * time.Second} // GETリクエスト発行 rsp, err := cli.Get(uStr) if err != nil { fmt.Println(err) return } // 関数を抜ける際に必ずresponseをcloseするようにdeferでcloseを呼ぶ defer rsp.Body.Close() // レスポンスを取得し出力 body, _ := ioutil.ReadAll(rsp.Body) fmt.Println(string(body)) }
21行目でClient構造体を生成し、24行目で生成したClient型を使用してGetメソッドを発行しています。
このコードを実行すると、先ほどとまったく同じ出力が得られます。
Client構造体のDoメソッドを使用する
Client構造体にはもう一つ、Getメソッドを送信するためのDoメソッドが実装されています。
Doメソッドは、引数にurl文字列ではなくRequest型の構造体を取ります。
Request構造体は、サーバーによって受信された、あるいはクライアントによって送信されるHTTPリクエストを表します。
Request構造体は、リクエストヘッダやformデータ、net.url.URL構造体型のURL情報など、様々な情報をフィールドとして保持しています。
したがって、Getメソッドでは制御できない細かい設定が必要な場合にDoメソッドを使用することになります。
それでは実装例をみてみましょう。
先ほどと同様、server.goは変更せずclient.goを以下のように書き換えてみてください。
package main import ( "fmt" "io/ioutil" "net/http" "net/http/httputil" "net/url" "time" ) func main() { // URLを生成 u := &url.URL{} u.Scheme = "http" u.Host = "localhost:8080" // url文字列 uStr := u.String() // タイムアウトを30秒に指定してClient構造体を生成 cli := &http.Client{Timeout: time.Duration(30) * time.Second} // 生成したURLを元にRequest構造体を生成 req, _ := http.NewRequest("GET", uStr, nil) // リクエストヘッダの内容を出力 header1, _ := httputil.DumpRequestOut(req, true) fmt.Println(string(header1)) // リクエストにヘッダ情報のを追加(User-Agent) req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MAFSJS; rv:11.0) like Gecko") // リクエストヘッダの内容を出力 header2, _ := httputil.DumpRequestOut(req, true) fmt.Println(string(header2)) // GETリクエスト発行 rsp, err := cli.Do(req) if err != nil { fmt.Println(err) return } // 関数を抜ける際に必ずresponseをcloseするようにdeferでcloseを呼ぶ defer rsp.Body.Close() // レスポンスを取得し出力 body, _ := ioutil.ReadAll(rsp.Body) fmt.Println(string(body)) }
25行目でhttp.NewRequestを生成し、32行目でHeaderプロパティのUser-Agent情報を書き換えています。
また、28-29行目と35-36行目で更新前後のリクエストヘッダ情報をそれぞれ出力し、内容を比較できるようにしています。
なお、リクエストヘッダの内容の出力には、httputil.DumpRequestOut関数を使用しています。
それではclient.goを実行してみてください。
【デフォルトのヘッダ情報】
GET / HTTP/1.1
Host: localhost:8080
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
【変更後のヘッダ情報】
GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MAFSJS; rv:11.0) like Gecko
Accept-Encoding: gzip
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Go | net/httpパッケージ</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>これはnet/httpで実装したlocalhostのWEBサーバです</p>
</body>
</html>
期待通り、【デフォルトのヘッダ情報】と【変更後のヘッダ情報】User-Agentの情報が変更されていることが確認できるかと思います。
終わりに
GETリクエストの送信には3通りの方法があることを解説しました。
デフォルトクライアントで問題なければhttp.Get関数を、タイムアウト値やクッキー値を設定したければClient構造体のGetメソッドを、より細かくリクエストを制御したい場合はClient構造体のDoメソッドを使用する、と整理していただければ問題ありません。
次回は引き続きPOSTリクエストの送信方法を解説したいと思います。