ウェブサイト検索

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

packagemain を追加して、プログラムが実行可能なプログラムとしてコンパイルされ、さまざまなパッケージに 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.NewRequesthttp.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.DefaultClientDo メソッドを使用してリクエストをサーバーに送信します。 http.DefaultClient 値は Go のデフォルトの HTTP クライアントであり、http.Get で使用しているものと同じです。ただし、今回はこれを直接使用して、http.Request を送信するように指示しています。 HTTP クライアントの Do メソッドは、http.Get 関数から受け取ったのと同じ値を返すため、同じ方法で応答を処理できます。

リクエストの結果を出力した後、io.ReadAll 関数を使用して HTTP レスポンスの Body を読み取ります。 Bodyio.ReadCloser 値であり、io.Readerio.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.DefaultClientDo メソッドを使用してリクエストを作成し、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.Getcontent-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.MethodPostPOST メソッドを使用するように更新され、リクエスト本文は、 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.NewRequesthttp.DefaultClientDo メソッドを使用して、GET リクエストを作成しました。次に、リクエストを更新して、bytes.NewReader を使用した本文を持つ POST リクエストにしました。最後に、http.RequestHeader フィールドで Set メソッドを使用して、リクエストの Content-Type を設定しました。 > ヘッダーを追加し、Go のデフォルト クライアントを使用する代わりに独自の HTTP クライアントを作成して、リクエストの継続時間に 30 秒のタイムアウトを設定します。

net/http パッケージには、このチュートリアルで使用した機能だけではありません。また、http.Get 関数と同様に、POST リクエストの作成に使用できる http.Post 関数も含まれています。このパッケージでは、Cookie の保存と取得などの機能もサポートされています。

このチュートリアルは、DigitalOcean の「How to Code in Go」シリーズの一部でもあります。このシリーズでは、Go の初めてのインストールから言語自体の使用方法まで、Go に関する多くのトピックを取り上げます。