著者は、 Diversity in Tech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
プログラムが別のプログラムと通信する必要がある場合、多くの開発者はHTTPを使用します。 Goの強みの1つはその標準ライブラリの幅広さであり、HTTPも例外ではありません。 Go net / http パッケージは、 HTTPサーバーの作成をサポートするだけでなく、クライアントとしてHTTPリクエストを作成することもできます。
このチュートリアルでは、HTTPサーバーに対していくつかのタイプのHTTPリクエストを作成するプログラムを作成します。 まず、デフォルトのGoHTTPクライアントを使用してGET
リクエストを作成します。 次に、プログラムを拡張して、本文を使用してPOST
リクエストを作成します。 最後に、POST
リクエストをカスタマイズしてHTTPヘッダーを含め、リクエストに時間がかかりすぎる場合にトリガーされるタイムアウトを追加します。
前提条件
このチュートリアルに従うには、次のものが必要です。
- バージョン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
関数は、リクエストを行うためにプログラムで追加のセットアップを行う必要がないため便利です。 クイックリクエストを1回行う必要がある場合は、http.Get
が最適なオプションです。
プログラムの作成を開始するには、プログラムのディレクトリを保持するためのディレクトリが必要です。 このチュートリアルでは、projects
という名前のディレクトリを使用します。
まず、projects
ディレクトリを作成し、次の場所に移動します。
mkdir projects cd projects
次に、プロジェクトのディレクトリを作成し、そこに移動します。 この場合、ディレクトリhttpclient
を使用します。
mkdir httpclient cd httpclient
httpclient
ディレクトリ内で、nano
またはお気に入りのエディタを使用して、main.go
ファイルを開きます。
nano main.go
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サーバーを起動するためのゴルーチンを設定します。
main.go
... 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
でリッスンするようにも設定されています。 最後に、サーバーゴルーチンを起動すると、プログラムはtime.Sleep
を短時間使用します。 このスリープ時間により、HTTPサーバーは、起動して次に行う要求への応答の提供を開始するために必要な時間を確保できます。
ここで、main
関数でも、fmt.Sprintf
を使用してリクエストURLを設定し、http://localhost
ホスト名とサーバーがリッスンしているserverPort
値を組み合わせます。 次に、http.Get
を使用して、以下に示すように、そのURLにリクエストを送信します。
main.go
... 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
次の出力が表示されます。
Outputserver: 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/ioutil
も含める必要があります。
次に、main.go
ファイルを再度開き、プログラムを更新して、以下に示すようにhttp.Request
の使用を開始します。
main.go
package main import ( ... "io/ioutil" ... ) ... 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
メソッドを使用するようにします。 :
main.go
... 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 := ioutil.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
関数から受け取ったのと同じ値を返すため、同じ方法で応答を処理できます。
リクエスト結果を出力したら、 ioutil.ReadAll 関数を使用して、HTTP応答のBody
を読み取ります。 Body
はio.ReadCloser値であり、io.Readerとio.Closerの組み合わせであり、本体のio.Reader値から読み取ることができるものを使用するデータ。 ioutil.ReadAll
関数は、io.Reader
からデータの最後に到達するか、error
に遭遇するまで読み取るため、便利です。 次に、fmt.Printf
を使用して印刷できる[]byte
値、または検出したerror
値としてデータを返します。
更新したプログラムを実行するには、変更を保存してgo run
コマンドを使用します。
go run main.go
今回は、出力は以前と非常によく似ているはずですが、次の1つが追加されています。
Outputserver: 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
Body
を出力に出力します。 。
ただし、HTTPプロトコルは、プログラム間の通信にGET
要求以上のものを使用します。 GET
リクエストは、他のプログラムから情報を受信する場合に役立ちますが、プログラムからサーバーに情報を送信する場合は、別のHTTPメソッドであるPOST
メソッドを使用できます。 。
POSTリクエストの送信
REST API では、GET
リクエストはサーバーから情報を取得するためにのみ使用されるため、プログラムがREST APIに完全に参加するには、プログラムが[X200Xの送信もサポートする必要があります。 ]リクエスト。 POST
リクエストは、GET
リクエストのほぼ逆であり、クライアントはリクエストの本文でサーバーにデータを送信します。
このセクションでは、プログラムを更新して、GET
リクエストではなくPOST
リクエストとしてリクエストを送信します。 POST
リクエストにはリクエストの本文が含まれ、サーバーを更新して、クライアントからのリクエストに関する詳細情報を出力します。
これらの更新を開始するには、main.go
ファイルを開き、使用するいくつかの新しいパッケージをimport
ステートメントに追加します。
main.go
... import ( "bytes" "errors" "fmt" "io/ioutil" "net/http" "os" "strings" "time" ) ...
次に、サーバーハンドラー関数を更新して、クエリ文字列値、ヘッダー値、リクエスト本文など、着信するリクエストに関するさまざまな情報を出力します。
main.go
... 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 := ioutil.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
という名前のヘッダーの値を取得します。 また、for
ループとr.Header
を使用して、サーバーが受信した各HTTPヘッダーの名前と値を出力します。 この情報は、クライアントまたはサーバーが期待どおりに動作していない場合の問題のトラブルシューティングに役立ちます。 最後に、ioutil.ReadAll
関数を使用して、r.Body
のHTTPリクエストの本文を読み取りました。
サーバーハンドラー関数を更新した後、main
関数のリクエストコードを更新して、リクエスト本文を含むPOST
リクエストを送信するようにします。
main.go
... 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
値です。 この例では、値は標準のstring
ではなく[]byte
として表されます。これは、encoding/json
パッケージを使用してJSONデータをエンコードすると、[X172X string
の代わりにバック。
次の値であるbodyReader
は、jsonBody
データをラップするbytes.Readerです。 http.Request
本体では、値がio.Reader
である必要があり、jsonBody
の[]byte
値はio.Reader
を実装していないため、それ自体をリクエストボディとして使用することはできません。 bytes.Reader
値は、そのio.Reader
インターフェイスを提供するために存在するため、jsonBody
値を要求の本文として使用できます。
requestURL
値も更新され、id=1234
クエリ文字列値が含まれるようになりました。これは主に、クエリ文字列値を他の標準URLコンポーネントとともにリクエストURLに含める方法を示すためです。
最後に、http.NewRequest
関数呼び出しがPOST
メソッドとhttp.MethodPost
を使用するように更新され、nil
の最後のパラメーターを更新することでリクエスト本文が含まれます。本文はbodyReader
、JSONデータio.Reader
です。
変更を保存したら、go run
を使用してプログラムを実行できます。
go run main.go
追加情報を表示するためのサーバーの更新により、出力は以前より長くなります。
Outputserver: 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
リクエストの代わりにHTTPPOST
リクエストを送信するようにプログラムを更新しました。 また、bytes.Reader
によって読み取られる[]byte
データを含むリクエスト本文を送信するようにプログラムを更新しました。 最後に、サーバーハンドラー関数を更新して、HTTPクライアントが行っている要求に関する詳細情報を出力しました。
通常、HTTPリクエストでは、クライアントまたはサーバーは、本文で送信しているコンテンツのタイプを相手に通知します。 ただし、最後の出力で見たように、HTTPリクエストには、本文のデータを解釈する方法をサーバーに指示するContent-Type
ヘッダーが含まれていませんでした。 次のセクションでは、送信するデータの種類をサーバーに通知するためのContent-Type
ヘッダーの設定など、HTTPリクエストをカスタマイズするためのいくつかの更新を行います。
HTTPリクエストのカスタマイズ
時間の経過とともに、HTTP要求と応答は、クライアントとサーバー間でさまざまなデータを送信するために使用されてきました。 ある時点で、HTTPクライアントは、HTTPサーバーから受信しているデータがHTMLであり、正しい可能性が高いと想定する可能性があります。 ただし、HTML、JSON、音楽、ビデオ、またはその他の任意の数のデータ型である可能性があります。 HTTPを介して送信されるデータに関する詳細情報を提供するために、プロトコルにはHTTPヘッダーが含まれており、それらの重要なヘッダーの1つはContent-Type
ヘッダーです。 このヘッダーは、サーバー(またはデータの方向によってはクライアント)に、受信しているデータを解釈する方法を指示します。
このセクションでは、プログラムを更新してHTTPリクエストにContent-Type
ヘッダーを設定し、サーバーがJSONデータを受信していることを認識できるようにします。 また、Goのデフォルトのhttp.DefaultClient
以外のHTTPクライアントを使用するようにプログラムを更新して、リクエストの送信方法をカスタマイズできるようにします。
これらの更新を行うには、main.go
ファイルを再度開き、main
関数を次のように更新します。
main.go
... 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のメディアタイプとして定義されています。 このように、サーバーはリクエストを受信すると、本文をJSONとして解釈し、たとえばXMLとして解釈しないことを認識します。
次の更新は、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
を使用するようにリクエストを更新すると、その[を使用していました。 X162X]直接。 現在の唯一の違いは、今回使用しているhttp.Client
値を作成したことです。
次に、ファイルを保存し、go run
を使用してプログラムを実行します。
go run main.go
出力は前の出力と非常に似ているはずですが、コンテンツタイプに関する詳細情報が含まれています。
Outputserver: 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
ヘッダーがクライアントから送信されていることがわかります。 これは、JSONとXMLAPIの両方を同時に提供する同じHTTPリクエストパスを持つことができる方法です。 リクエストのコンテンツタイプを指定することにより、サーバーとクライアントはデータを異なる方法で解釈できます。
ただし、この例では、構成したクライアントタイムアウトはトリガーされません。 リクエストに時間がかかりすぎてタイムアウトがトリガーされたときに何が起こるかを確認するには、main.go
ファイルを開き、time.Sleep
関数呼び出しをHTTPサーバーハンドラー関数に追加します。 次に、time.Sleep
を指定したタイムアウトより長く持続させます。 この場合、35秒間設定します。
main.go
... 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秒のタイムアウトに達するまで完了しません。
Outputserver: 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秒の制限に達します。 これにより、client.Do
メソッド呼び出しが失敗し、context deadline exceeded
エラーが発生します。これは、要求の30秒の期限が過ぎたためです。 次に、os.Exit(1)
を使用して、プログラムは1
の障害ステータスコードで終了します。
このセクションでは、Content-Type
ヘッダーを追加して、HTTPリクエストをカスタマイズするようにプログラムを更新しました。 また、プログラムを更新して、30秒のタイムアウトで新しいhttp.Client
を作成し、そのクライアントを使用してHTTPリクエストを作成しました。 また、HTTPリクエストハンドラーにtime.Sleep
を追加して、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クライアントを作成してリクエストの期間。
net / http パッケージには、このチュートリアルで使用した機能以上のものが含まれています。 また、http.Get
関数と同様に、POST
リクエストを行うために使用できるhttp.Post関数も含まれています。 このパッケージは、他の機能の中でも特に、Cookieの保存と取得をサポートしています。
このチュートリアルは、 DigitalOcean How to Code inGoシリーズの一部でもあります。 このシリーズでは、Goの初めてのインストールから、言語自体の使用方法まで、Goに関する多くのトピックを取り上げています。