Go で HTTP リクエストを行う方法
著者は、Write for DOnations プログラムの一環として寄付を受け取るために、Diversity in Tech Fund を選択しました。
導入
プログラムが別のプログラムと通信する必要がある場合、多くの開発者は HTTP を使用します。 Go の強みの 1 つはその標準ライブラリの広さであり、HTTP も例外ではありません。 Go net/http
パッケージは、HTTP サーバーの作成をサポートするだけでなく、クライアントとして HTTP リクエストを行うこともできます。
このチュートリアルでは、HTTP サーバーに対して数種類の HTTP リクエストを行うプログラムを作成します。まず、デフォルトの Go HTTP クライアントを使用して GET
リクエストを作成します。次に、本体を含む POST
リクエストを作成するようにプログラムを拡張します。最後に、POST
リクエストをカスタマイズして HTTP ヘッダーを含め、リクエストに時間がかかりすぎる場合にトリガーされるタイムアウトを追加します。
前提条件
このチュートリアルに従うには、次のものが必要です。
- Go バージョン 1.16 以降がインストールされている。これを設定するには、お使いのオペレーティング システムの Go のインストール方法チュートリアルに従ってください。
- Go で HTTP サーバーを作成する経験。これについては、チュートリアル「Go で HTTP サーバーを作成する方法」を参照してください。
- ゴルーチンと読み取りチャネルに関する知識。詳細については、チュートリアル「Go で複数の関数を同時に実行する方法」を参照してください。
- HTTP リクエストがどのように構成され、送信されるかを理解することをお勧めします。
GETリクエストを行う
Go net/http
パッケージには、クライアントとして使用するためのいくつかの方法があります。 http.Get
などの関数を備えた共通のグローバル HTTP クライアントを使用して、URL と本文のみを含む HTTP GET
リクエストをすばやく作成することも、 http.Request
を使用して、個々のリクエストの特定の側面のカスタマイズを開始します。このセクションでは、http.Get
を使用して HTTP リクエストを作成する初期プログラムを作成し、次にデフォルトの HTTP で http.Request
を使用するようにプログラムを更新します。クライアント。
http.Get
を使用してリクエストを行う
プログラムの最初の反復では、http.Get
関数を使用して、プログラム内で実行する HTTP サーバーにリクエストを送信します。 http.Get
関数は、リクエストを行うためにプログラムに追加の設定を必要としないため便利です。単一の簡単なリクエストを行う必要がある場合は、http.Get
が最適なオプションである可能性があります。
プログラムの作成を開始するには、プログラムのディレクトリを保存するディレクトリが必要です。このチュートリアルでは、projects
という名前のディレクトリを使用します。
まず、projects
ディレクトリを作成し、そこに移動します。
mkdir projects
cd projects
次に、プロジェクトのディレクトリを作成し、そこに移動します。この場合、ディレクトリ httpclient
を使用します。
mkdir httpclient
cd httpclient
httpclient
ディレクトリ内で、nano
またはお気に入りのエディタを使用して main.go
ファイルを開きます。
nano main.go
main.go
ファイルに次の行を追加することから始めます。
package main
import (
"errors"
"fmt"
"net/http"
"os"
"time"
)
const serverPort = 3333
package
名 main
を追加して、プログラムが実行可能なプログラムとしてコンパイルされ、さまざまなパッケージに import
ステートメントを含めます。このプログラムで使用します。その後、値 3333
を持つ serverPort
という const
を作成します。これは、HTTP サーバーがリッスンするポートとして使用します。 HTTP クライアントが接続するポート。
次に、main.go
ファイルに main
関数を作成し、HTTP サーバーを起動するための goroutine を設定します。
...
func main() {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("server: %s /\n", r.Method)
})
server := http.Server{
Addr: fmt.Sprintf(":%d", serverPort),
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
fmt.Printf("error running http server: %s\n", err)
}
}
}()
time.Sleep(100 * time.Millisecond)
HTTP サーバーは、ルート /
パスが要求されるたびに、fmt.Printf
を使用して受信リクエストに関する情報を出力するように設定されています。また、serverPort
でリッスンするように設定されています。最後に、サーバーの goroutine を起動すると、プログラムは短時間 time.Sleep
を使用します。このスリープ時間により、HTTP サーバーが起動して、次に行うリクエストに対する応答の提供を開始するのに必要な時間が確保されます。
次に、main
関数でも、fmt.Sprintf
を使用してリクエスト URL を設定し、http://localhost
ホスト名と serverPort
サーバーがリッスンしている値。次に、以下に示すように、http.Get
を使用してその URL にリクエストを送信します。
...
requestURL := fmt.Sprintf("http://localhost:%d", serverPort)
res, err := http.Get(requestURL)
if err != nil {
fmt.Printf("error making http request: %s\n", err)
os.Exit(1)
}
fmt.Printf("client: got response!\n")
fmt.Printf("client: status code: %d\n", res.StatusCode)
}
http.Get
関数が呼び出されると、Go はデフォルトの HTTP クライアントを使用して指定された URL に対して HTTP リクエストを作成し、http.Response
または を返します。 >error
リクエストが失敗した場合の値。リクエストが失敗した場合、エラーが出力され、os.Exit
を使用してエラー コード 1
でプログラムが終了します。リクエストが成功すると、プログラムは応答を受け取ったことと受信した HTTP ステータス コードを出力します。
完了したら、ファイルを保存して閉じます。
プログラムを実行するには、 go run
コマンドを使用し、それに main.go
ファイルを指定します。
go run main.go
次の出力が表示されます。
server: GET /
client: got response!
client: status code: 200
出力の最初の行では、サーバーはクライアントから /
パスに対する GET
リクエストを受信したことを出力します。次に、次の 2 行は、クライアントがサーバーから応答を受け取り、その応答のステータス コードが 200
であったことを示しています。
http.Get
関数は、このセクションで作成したような簡単な HTTP リクエストに役立ちます。ただし、http.Request
では、リクエストをカスタマイズするための幅広いオプションが提供されます。
http.Request
を使用してリクエストを行う
http.Get
とは対照的に、 http.Request
関数では、HTTP メソッドとリクエストされる URL 以外にも、リクエストをより詳細に制御できます。まだ追加機能は使用しませんが、ここで http.Request
を使用することで、このチュートリアルの後半でこれらのカスタマイズを追加できるようになります。
コードの最初の更新は、fmt.Fprintf
を使用して偽の JSON データ応答を返すように HTTP サーバー ハンドラーを変更することです。これが完全な HTTP サーバーの場合、このデータは Go の encoding/json
パッケージを使用して生成されます。 Go での JSON の使用について詳しく知りたい場合は、Go で JSON を使用する方法のチュートリアルをご覧ください。さらに、この更新で後ほど使用するために、インポートとして io
パッケージを含める必要もあります。
次に、main.go
ファイルを再度開き、以下に示すように http.Request
の使用を開始するようにプログラムを更新します。
package main
import (
...
"io"
...
)
...
func main() {
...
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("server: %s /\n", r.Method)
fmt.Fprintf(w, `{"message": "hello!"}`)
})
...
ここで、HTTP リクエスト コードを更新して、http.Get
を使用してサーバーにリクエストを行う代わりに、http.NewRequest
と http.DefaultClient
を使用するようにします。 の Do
メソッド:
...
requestURL := fmt.Sprintf("http://localhost:%d", serverPort)
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
fmt.Printf("client: could not create request: %s\n", err)
os.Exit(1)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
os.Exit(1)
}
fmt.Printf("client: got response!\n")
fmt.Printf("client: status code: %d\n", res.StatusCode)
resBody, err := io.ReadAll(res.Body)
if err != nil {
fmt.Printf("client: could not read response body: %s\n", err)
os.Exit(1)
}
fmt.Printf("client: response body: %s\n", resBody)
}
この更新では、http.NewRequest
関数を使用して http.Request
値を生成するか、値を作成できない場合にエラーを処理します。ただし、http.Get
関数とは異なり、http.NewRequest
関数は HTTP リクエストをサーバーにすぐには送信しません。リクエストはすぐには送信されないため、送信前にリクエストに必要な変更を加えることができます。
http.Request
を作成して構成したら、http.DefaultClient
の Do
メソッドを使用してリクエストをサーバーに送信します。 http.DefaultClient
値は Go のデフォルトの HTTP クライアントであり、http.Get
で使用しているものと同じです。ただし、今回はこれを直接使用して、http.Request
を送信するように指示しています。 HTTP クライアントの Do
メソッドは、http.Get
関数から受け取ったのと同じ値を返すため、同じ方法で応答を処理できます。
リクエストの結果を出力した後、io.ReadAll
関数を使用して HTTP レスポンスの Body
を読み取ります。 Body
は io.ReadCloser
値であり、io.Reader
と io.Closer
を組み合わせたものです。つまり、 io.Reader
値から読み取ることができるものであれば何でも使用して、本体のデータを読み取ることができます。 io.ReadAll
関数は、データの最後に到達するか error
が発生するまで io.Reader
から読み取るため便利です。 >。次に、データを fmt.Printf
を使用して印刷できる []byte
値として返すか、または発生した error
値として返します。
更新されたプログラムを実行するには、変更を保存し、go run
コマンドを使用します。
go run main.go
今回の出力は以前とよく似ていますが、次の点が追加されています。
server: GET /
client: got response!
client: status code: 200
client: response body: {"message": "hello!"}
最初の行では、サーバーが /
パスへの GET
リクエストをまだ受信していることがわかります。クライアントはサーバーから 200
応答も受信しますが、サーバーの応答の Body
も読み取って出力します。より複雑なプログラムでは、サーバーから本文として受け取った {"message": "hello!"}
値を取得し、encoding/json を使用して JSON として処理できます。
パッケージ。
このセクションでは、さまざまな方法で HTTP リクエストを送信する HTTP サーバーを使用するプログラムを作成しました。まず、http.Get
関数を使用して、サーバーの URL のみを使用してサーバーに GET
リクエストを送信しました。次に、http.NewRequest
を使用して http.Request
値を作成するようにプログラムを更新しました。これを作成したら、Go のデフォルト HTTP クライアント http.DefaultClient
の Do
メソッドを使用してリクエストを作成し、http.Response
を出力します。 > 本文
を出力に追加します。
ただし、HTTP プロトコルはプログラム間の通信に GET
リクエストだけを使用するだけではありません。 GET
リクエストは、他のプログラムから情報を受信する場合に便利ですが、別の HTTP メソッドである POST
メソッドは、他のプログラムから情報を送信する場合に使用できます。プログラムをサーバーに送信します。
POSTリクエストの送信
REST API では、GET
リクエストはサーバーから情報を取得するためにのみ使用されるため、プログラムが REST API に完全に参加するには、プログラムが POST
の送信もサポートする必要があります。リクエスト。 POST
リクエストは、GET
リクエストのほぼ逆で、クライアントはリクエストの本文でデータをサーバーに送信します。
このセクションでは、リクエストを GET
リクエストではなく POST
リクエストとして送信するようにプログラムを更新します。 POST
リクエストにはリクエスト本文が含まれており、サーバーを更新してクライアントからのリクエストに関する詳細情報を出力します。
これらの更新を開始するには、main.go
ファイルを開き、使用するいくつかの新しいパッケージを import
ステートメントに追加します。
...
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
...
次に、サーバー ハンドラー関数を更新して、クエリ文字列値、ヘッダー値、リクエスト本文など、受信するリクエストに関するさまざまな情報を出力します。
...
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("server: %s /\n", r.Method)
fmt.Printf("server: query id: %s\n", r.URL.Query().Get("id"))
fmt.Printf("server: content-type: %s\n", r.Header.Get("content-type"))
fmt.Printf("server: headers:\n")
for headerName, headerValue := range r.Header {
fmt.Printf("\t%s = %s\n", headerName, strings.Join(headerValue, ", "))
}
reqBody, err := io.ReadAll(r.Body)
if err != nil {
fmt.Printf("server: could not read request body: %s\n", err)
}
fmt.Printf("server: request body: %s\n", reqBody)
fmt.Fprintf(w, `{"message": "hello!"}`)
})
...
サーバーの HTTP リクエスト ハンドラーの今回の更新では、受信リクエストに関する情報を確認するために役立つ fmt.Printf
ステートメントをさらにいくつか追加します。 r.URL.Query().Get を使用します。
は id
という名前のクエリ文字列値を取得し、r.Header.Get
は content-type
というヘッダーの値を取得します。 >。また、r.Header
を指定した for
ループを使用して、サーバーが受信した各 HTTP ヘッダーの名前と値を出力します。この情報は、クライアントまたはサーバーが期待どおりに動作しない場合の問題のトラブルシューティングに役立ちます。最後に、io.ReadAll
関数を使用して、r.Body
内の HTTP リクエストの本文を読み取りました。
サーバー ハンドラー関数を更新した後、main
関数のリクエスト コードを更新して、リクエスト本文を含む POST
リクエストを送信するようにします。
...
time.Sleep(100 * time.Millisecond)
jsonBody := []byte(`{"client_message": "hello, server!"}`)
bodyReader := bytes.NewReader(jsonBody)
requestURL := fmt.Sprintf("http://localhost:%d?id=1234", serverPort)
req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader)
...
main
関数のリクエストの更新で、定義している新しい値の 1 つが jsonBody
値です。この例では、encoding/json
パッケージを使用してエンコードする場合、値は標準の string
ではなく []byte
として表されます。 JSON データの場合、string
の代わりに []byte
が返されます。
次の値 bodyReader
は、jsonBody
データをラップする bytes.Reader
です。 http.Request
本文の値は io.Reader
であり、jsonBody
の []byte
である必要があります。 value は io.Reader
を実装していないため、それを単独でリクエスト本文として使用することはできません。 bytes.Reader
値は、io.Reader
インターフェイスを提供するために存在するため、jsonBody
値をリクエスト本文として使用できます。
requestURL
値も更新され、id=1234
クエリ文字列値が含まれます。これは主に、クエリ文字列値を他の標準とともにリクエスト URL に含めることができる方法を示すためです。 URL コンポーネント。
最後に、http.NewRequest
関数呼び出しは、http.MethodPost
で POST
メソッドを使用するように更新され、リクエスト本文は、 nil
body から bodyReader
への最後のパラメータ、JSON データ io.Reader
。
変更を保存したら、go run
を使用してプログラムを実行できます。
go run main.go
追加情報を表示するためにサーバーを更新したため、出力は以前より長くなります。
server: POST /
server: query id: 1234
server: content-type:
server: headers:
Accept-Encoding = gzip
User-Agent = Go-http-client/1.1
Content-Length = 36
server: request body: {"client_message": "hello, server!"}
client: got response!
client: status code: 200
client: response body: {"message": "hello!"}
サーバーからの最初の行は、リクエストが POST
リクエストとして /
パスに送信されていることを示しています。 2 行目は、リクエストの URL に追加した id
クエリ文字列値の 1234
値を示します。 3 行目は、クライアントが送信した Content-Type
ヘッダーの値を示していますが、このリクエストでは空になっています。
4 行目は、上記の出力とは若干異なる場合があります。 Go では、range
を使用して反復処理するときに map
値の順序が保証されないため、r.Headers
のヘッダーが出力される可能性があります。違う順番で出します。使用している Go のバージョンによっては、上記とは異なる User-Agent
バージョンが表示される場合もあります。
最後に、出力の最後の変更は、サーバーがクライアントから受信したリクエスト本文を表示していることです。その後、サーバーは encoding/json
パッケージを使用して、クライアントが送信した JSON データを解析し、応答を作成できます。
このセクションでは、GET
リクエストの代わりに HTTP POST
リクエストを送信するようにプログラムを更新しました。また、bytes.Reader
によって読み取られる []byte
データを含むリクエスト本文を送信するようにプログラムを更新しました。最後に、HTTP クライアントが行っているリクエストに関する詳細情報を出力するようにサーバー ハンドラー関数を更新しました。
通常、HTTP リクエストでは、クライアントまたはサーバーが本文で送信するコンテンツの種類を相手に伝えます。ただし、最後の出力で見たように、HTTP リクエストには、本文のデータを解釈する方法をサーバーに指示するための Content-Type
ヘッダーが含まれていませんでした。次のセクションでは、送信するデータの種類をサーバーに知らせるための Content-Type
ヘッダーの設定など、HTTP リクエストをカスタマイズするためにいくつかの更新を行います。
HTTP リクエストのカスタマイズ
時間の経過とともに、HTTP リクエストとレスポンスは、クライアントとサーバー間でさまざまなデータを送信するために使用されるようになりました。ある時点で、HTTP クライアントは、HTTP サーバーから受信しているデータが HTML であると想定し、それが正しい可能性が高くなります。ただし、現在では、HTML、JSON、音楽、ビデオ、またはその他のさまざまなデータ型が可能です。 HTTP 経由で送信されるデータに関する詳細情報を提供するために、プロトコルには HTTP ヘッダーが含まれており、それらの重要なヘッダーの 1 つが Content-Type
ヘッダーです。このヘッダーは、サーバー (またはデータの方向に応じてクライアント) に、受信するデータを解釈する方法を指示します。
このセクションでは、サーバーが JSON データを受信していることを認識できるように、HTTP リクエストに Content-Type
ヘッダーを設定するようにプログラムを更新します。また、Go のデフォルトの http.DefaultClient
以外の HTTP クライアントを使用するようにプログラムを更新して、リクエストの送信方法をカスタマイズできるようにします。
これらの更新を行うには、main.go
ファイルを再度開き、次のように main
関数を更新します。
...
req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader)
if err != nil {
fmt.Printf("client: could not create request: %s\n", err)
os.Exit(1)
}
req.Header.Set("Content-Type", "application/json")
client := http.Client{
Timeout: 30 * time.Second,
}
res, err := client.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
os.Exit(1)
}
...
この更新では、req.Header
を使用して http.Request
ヘッダーにアクセスし、Content-Type
ヘッダーの値をapplication/json
へのリクエスト。 application/json
メディア タイプは、メディア タイプのリストで JSON のメディア タイプとして定義されています。このようにして、サーバーはリクエストを受信したときに、本文を XML などではなく JSON として解釈することを認識します。
次の更新は、client
変数に独自の http.Client
インスタンスを作成することです。このクライアントでは、Timeout
値を 30 秒に設定します。これは、クライアントに対して行われたリクエストは 30 秒後に放棄され、応答の受信を停止することを示しているため、重要です。 Go のデフォルトの http.DefaultClient
ではタイムアウトが指定されていないため、そのクライアントを使用してリクエストを行うと、応答を受信するか、サーバーによって切断されるか、プログラムが終了するまで待機します。このように応答を待っているリクエストが多数ある場合は、コンピューター上の大量のリソースを使用している可能性があります。 Timeout
値を設定すると、定義した時間までにリクエストが待機する時間を制限します。
最後に、client
変数の Do
メソッドを使用するようにリクエストを更新しました。ずっと http.Client
値に対して Do
を呼び出しているため、ここでは他の変更を行う必要はありません。 Go のデフォルトの HTTP クライアントである http.DefaultClient
は、デフォルトで作成される単なる http.Client
です。したがって、http.Get
を呼び出したとき、関数は Do
メソッドを呼び出しており、リクエストを更新して http.DefaultClient
を使用したときは、 >、その http.Client
を直接使用していました。唯一の違いは、今回使用している http.Client
値を作成したことです。
次に、ファイルを保存し、go run
を使用してプログラムを実行します。
go run main.go
出力は前の出力とよく似ていますが、コンテンツ タイプに関する詳細情報が含まれています。
server: POST /
server: query id: 1234
server: content-type: application/json
server: headers:
Accept-Encoding = gzip
User-Agent = Go-http-client/1.1
Content-Length = 36
Content-Type = application/json
server: request body: {"client_message": "hello, server!"}
client: got response!
client: status code: 200
client: response body: {"message": "hello!"}
content-type
のサーバーからの値があり、クライアントによって送信されている Content-Type
ヘッダーがあることがわかります。これにより、同じ HTTP リクエスト パスで JSON と XML API の両方を同時に処理できるようになります。リクエストのコンテンツ タイプを指定することにより、サーバーとクライアントはデータを異なる方法で解釈できます。
ただし、この例では、設定したクライアント タイムアウトはトリガーされません。リクエストに時間がかかりすぎてタイムアウトがトリガーされた場合に何が起こるかを確認するには、main.go
ファイルを開き、time.Sleep
関数呼び出しを HTTP サーバー ハンドラー関数に追加します。 。 次に、time.Sleep
を指定したタイムアウトよりも長く継続します。この場合、35 秒に設定します。
...
func main() {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
...
fmt.Fprintf(w, `{"message": "hello!"}`)
time.Sleep(35 * time.Second)
})
...
}()
...
}
次に、変更を保存し、go run
を使用してプログラムを実行します。
go run main.go
今回実行すると、HTTP リクエストが完了するまで終了しないため、以前よりも終了に時間がかかります。 time.Sleep(35 * time.Second)
を追加したため、HTTP リクエストは 30 秒のタイムアウトに達するまで完了しません。
server: POST /
server: query id: 1234
server: content-type: application/json
server: headers:
Content-Type = application/json
Accept-Encoding = gzip
User-Agent = Go-http-client/1.1
Content-Length = 36
server: request body: {"client_message": "hello, server!"}
client: error making http request: Post "http://localhost:3333?id=1234": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
exit status 1
このプログラムの出力では、サーバーがリクエストを受信して処理したことがわかりますが、time.Sleep
関数呼び出しがある HTTP ハンドラー関数の最後に到達すると、サーバーは 35 秒間のスリープを開始しました。同時に、HTTP リクエストのタイムアウトがカウントダウンされ、HTTP リクエストが終了する前に 30 秒の制限に達します。この結果、リクエストの 30 秒の期限が過ぎたため、client.Do
メソッド呼び出しは context期限を超えました
エラーで失敗します。次に、プログラムは、os.Exit(1)
を使用して、失敗ステータス コード 1
で終了します。
このセクションでは、Content-Type
ヘッダーを追加することで HTTP リクエストをカスタマイズするようにプログラムを更新しました。また、プログラムを更新して、30 秒のタイムアウトを持つ新しい http.Client
を作成し、そのクライアントを使用して HTTP リクエストを作成しました。また、time.Sleep
を HTTP リクエスト ハンドラーに追加して、30 秒のタイムアウトをテストしました。最後に、多くのリクエストが永久にアイドル状態になる可能性を回避するには、タイムアウトを設定して独自の http.Client
値を使用することが重要である理由も説明しました。
結論
このチュートリアルでは、HTTP サーバーを使用して新しいプログラムを作成し、Go の net/http
パッケージを使用してそのサーバーに HTTP リクエストを送信しました。まず、http.Get
関数を使用して、Go のデフォルトの HTTP クライアントでサーバーに GET
リクエストを送信しました。次に、http.NewRequest
と http.DefaultClient
の Do
メソッドを使用して、GET
リクエストを作成しました。次に、リクエストを更新して、bytes.NewReader
を使用した本文を持つ POST
リクエストにしました。最後に、http.Request
の Header
フィールドで Set
メソッドを使用して、リクエストの Content-Type
を設定しました。 > ヘッダーを追加し、Go のデフォルト クライアントを使用する代わりに独自の HTTP クライアントを作成して、リクエストの継続時間に 30 秒のタイムアウトを設定します。
net/http
パッケージには、このチュートリアルで使用した機能だけではありません。また、http.Get
関数と同様に、POST
リクエストの作成に使用できる http.Post
関数も含まれています。このパッケージでは、Cookie の保存と取得などの機能もサポートされています。
このチュートリアルは、DigitalOcean の「How to Code in Go」シリーズの一部でもあります。このシリーズでは、Go の初めてのインストールから言語自体の使用方法まで、Go に関する多くのトピックを取り上げます。