序章

多くの開発者は、インターネット上でコンテンツを配布するためのサーバーの作成に少なくとも一部の時間を費やしています。 Hypertext Transfer Protocol (HTTP)は、猫の画像のリクエストであれ、現在読んでいるチュートリアルの読み込みのリクエストであれ、このコンテンツの多くを提供します。 Go標準ライブラリは、Webコンテンツを提供するHTTPサーバーを作成したり、それらのサーバーにHTTPリクエストを送信したりするための組み込みサポートを提供します。

このチュートリアルでは、Goの標準ライブラリを使用してHTTPサーバーを作成し、サーバーを拡張して、リクエストのクエリ文字列、本文、フォームデータからデータを読み取ります。 また、独自のHTTPヘッダーとステータスコードを使用してリクエストに応答するようにプログラムを更新します。

前提条件

このチュートリアルに従うには、次のものが必要です。

  • バージョン1.16以降をインストールしてください。 これを設定するには、オペレーティングシステムのGoチュートリアルをインストールする方法に従ってください。
  • curlを使用してWebリクエストを行う機能。 curlについて詳しくは、cURLを使用してファイルをダウンロードする方法をご覧ください。
  • GoでJSONを使用する方法チュートリアルにある、GoでのJSONの使用に関する知識。
  • Goの経験 context パッケージ。チュートリアルGoでコンテキストを使用する方法で入手できます。
  • チュートリアルGoで複数の関数を同時に実行する方法から得られるゴルーチンの実行とチャネルの読み取りの経験。
  • HTTP リクエストの作成方法と送信方法に精通していること(推奨)。

プロジェクトの設定

Goでは、ほとんどのHTTP機能は標準ライブラリの net / http パッケージによって提供され、残りのネットワーク通信はnetパッケージによって提供されます。 The net/http パッケージには、HTTPリクエストを作成する機能が含まれているだけでなく、それらのリクエストを処理するために使用できるHTTPサーバーも提供します。

このセクションでは、 http.ListenAndServe 関数を使用して、要求パスに応答するHTTPサーバーを起動するプログラムを作成します。 //hello. 次に、そのプログラムを拡張して、同じプログラムで複数のHTTPサーバーを実行します。

ただし、コードを作成する前に、プログラムのディレクトリを作成する必要があります。 多くの開発者は、プロジェクトを整理するためにディレクトリに保管しています。 このチュートリアルでは、という名前のディレクトリを使用します projects.

まず、 projects ディレクトリとそれに移動します:

  1. mkdir projects
  2. cd projects

次に、プロジェクトのディレクトリを作成し、そのディレクトリに移動します。 この場合、ディレクトリを使用します httpserver:

  1. mkdir httpserver
  2. cd httpserver

これで、プログラムのディレクトリが作成され、 httpserver ディレクトリを使用すると、HTTPサーバーの実装を開始できます。

リクエストのリッスンと応答の提供

Go HTTPサーバーには、2つの主要なコンポーネントが含まれています。HTTPクライアントからの要求をリッスンするサーバーと、それらの要求に応答する1つ以上の要求ハンドラーです。 このセクションでは、関数を使用することから始めます http.HandleFunc サーバーへの要求を処理するために呼び出す関数をサーバーに通知します。 次に、を使用します http.ListenAndServe サーバーを起動し、新しいHTTPリクエストをリッスンし、設定したハンドラー関数を使用してサーバーにサービスを提供するように指示する関数。

さて、 httpserver 作成したディレクトリ、使用 nano、またはお気に入りのエディタを開いて main.go ファイル:

  1. nano main.go

の中に main.go ファイル、2つの関数を作成します、 getRootgetHello、ハンドラー関数として機能します。 次に、を作成します main 関数を使用して、リクエストハンドラーを設定します。 http.HandleFunc それを渡すことによって機能 / のパス getRoot ハンドラー関数と /hello のパス getHello ハンドラー関数。 ハンドラーを設定したら、 http.ListenAndServe サーバーを起動してリクエストをリッスンする機能。

次のコードをファイルに追加して、プログラムを開始し、ハンドラーを設定します。

main.go
package main

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
)

func getRoot(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("got / request\n")
	io.WriteString(w, "This is my website!\n")
}
func getHello(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("got /hello request\n")
	io.WriteString(w, "Hello, HTTP!\n")
}

この最初のコードチャンクでは、 package Goプログラムの場合、 import プログラムに必要なパッケージ、および2つの関数を作成します。 getRoot 機能と getHello 関数。 これらの関数は両方とも同じ関数シグネチャを持ち、同じ引数を受け入れます。 http.ResponseWriter 値と *http.Request 価値。 この関数シグネチャはHTTPハンドラー関数に使用され、http.HandlerFuncとして定義されます。 サーバーに対して要求が行われると、サーバーは、行われた要求に関する情報を使用してこれら2つの値を設定し、それらの値を使用してハンドラー関数を呼び出します。

http.HandlerFunc http.ResponseWriter 値(名前付き w ハンドラー内)は、応答の本文やステータスコードなど、要求を行ったクライアントに書き戻される応答情報を制御するために使用されます。 次に、 * http.Request 値(名前付き r ハンドラー内)は、サーバーに着信したリクエストに関する情報を取得するために使用されます。たとえば、 POST リクエストまたはリクエストを行ったクライアントに関する情報。

今のところ、両方のHTTPハンドラーで、 fmt.Printf ハンドラー関数のリクエストが入ったときに印刷するには、 http.ResponseWriter 応答本文にテキストを送信します。 The http.ResponseWriterio.Writerです。これは、そのインターフェイスに書き込むことができるものなら何でも使用して、応答本文に書き込むことができることを意味します。 この場合、 io.WriteString 関数を使用して、本文への応答を書き込みます。

今、あなたのを開始することによってあなたのプログラムを作成し続けます main 関数:

main.go
...
func main() {
	http.HandleFunc("/", getRoot)
	http.HandleFunc("/hello", getHello)

	err := http.ListenAndServe(":3333", nil)
...

の中に main 関数、あなたはへの2つの呼び出しがあります http.HandleFunc 関数。 関数を呼び出すたびに、デフォルトのサーバーマルチプレクサで特定の要求パスのハンドラ関数が設定されます。 サーバーマルチプレクサはhttp.Handlerであり、要求パスを調べて、そのパスに関連付けられた特定のハンドラ関数を呼び出すことができます。 したがって、プログラムでは、デフォルトのサーバーマルチプレクサにを呼び出すように指示しています。 getRoot 誰かが要求したときに機能する / パスと getHello 誰かが要求したときに機能する /hello 道。

ハンドラーを設定したら、 http.ListenAndServe 関数。オプションで特定のポートで着信要求をリッスンするようにグローバルHTTPサーバーに指示します。 http.Handler. プログラムでは、サーバーにリッスンするように指示します ":3333". コロンの前にIPアドレスを指定しないことにより、サーバーはコンピューターに関連付けられているすべてのIPアドレスをリッスンし、ポートでリッスンします 3333. ネットワークポートなど 3333 ここでは、1台のコンピューターが同時に多くのプログラムを相互に通信させる方法です。 各プログラムは独自のポートを使用するため、クライアントが特定のポートに接続すると、コンピューターはそれを送信するプログラムを認識します。 接続のみを許可したい場合 localhost、IPアドレスのホスト名 127.0.0.1、代わりに言うことができます 127.0.0.1:3333.

君の http.ListenAndServe 関数も渡します nil の値 http.Handler パラメータ。 これは、 ListenAndServe 設定したものではなく、デフォルトのサーバーマルチプレクサを使用する関数。

The ListenAndServe はブロッキング呼び出しです。これは、プログラムが実行されるまで実行を継続しないことを意味します。 ListenAndServe 実行を終了します。 でも、 ListenAndServe プログラムの実行が終了するか、HTTPサーバーがシャットダウンするように指示されるまで、実行は終了しません。 それでも ListenAndServe がブロックされており、プログラムにサーバーをシャットダウンする方法が含まれていない場合でも、呼び出し方がいくつかあるため、エラー処理を含めることが重要です。 ListenAndServe 失敗する可能性があります。 だから、あなたにエラー処理を追加します ListenAndServe の中に main 示されているように機能します:

main.go
...

func main() {
	...
	err := http.ListenAndServe(":3333", nil)
  if errors.Is(err, http.ErrServerClosed) {
		fmt.Printf("server closed\n")
	} else if err != nil {
		fmt.Printf("error starting server: %s\n", err)
		os.Exit(1)
	<^>}
}

チェックしている最初のエラーhttp.ErrServerClosedは、サーバーがシャットダウンまたはクローズするように指示されたときに返されます。 これは通常、サーバーを自分でシャットダウンするために予想されるエラーですが、サーバーが出力で停止した理由を示すためにも使用できます。 2番目のエラーチェックでは、他のエラーをチェックします。 これが発生した場合、エラーが画面に出力され、エラーコードが次のようにプログラムが終了します。 1 を使用して os.Exit 関数。

プログラムの実行中に表示される可能性のあるエラーの1つは、 address already in use エラー。 このエラーは、次の場合に返される可能性があります。 ListenAndServe 別のプログラムがすでに使用しているため、指定したアドレスまたはポートをリッスンできません。 これは、ポートが一般的に使用され、コンピューター上の別のプログラムがそれを使用している場合に発生することがありますが、独自のプログラムの複数のコピーを複数回実行した場合にも発生する可能性があります。 このチュートリアルの作業中にこのエラーが表示された場合は、プログラムを再度実行する前に、前の手順でプログラムを停止したことを確認してください。

注: address already in use エラーが発生し、プログラムの別のコピーが実行されていない場合は、他のプログラムがそれを使用している可能性があります。 これが起こった場合、あなたが見るところはどこでも 3333 このチュートリアルで説明されているように、1024より上で65535より下の別の数値に変更します。 3334、 そしてさらに試みる。 それでもエラーが表示される場合は、使用されていないポートを探し続ける必要があるかもしれません。 動作するポートを見つけたら、このチュートリアルのすべてのコマンドにそれを使用します。

コードの準備ができたので、 main.go ファイルを作成し、を使用してプログラムを実行します go run. あなたが書いたかもしれない他のGoプログラムとは異なり、このプログラムはそれ自体ですぐに終了することはありません。 プログラムを実行したら、次のコマンドに進みます。

  1. go run main.go

プログラムはまだターミナルで実行されているため、サーバーと対話するには2番目のターミナルを開く必要があります。 以下のコマンドと同じ色のコマンドまたは出力が表示されている場合は、この2番目の端末で実行することを意味します。

この2番目の端末では、 curl プログラムを使用して、HTTPサーバーにHTTPリクエストを送信します。 curl は、さまざまなタイプのサーバーに要求を行うことができる多くのシステムにデフォルトで一般的にインストールされるユーティリティです。 このチュートリアルでは、これを使用してHTTPリクエストを作成します。 サーバーはコンピューターのポートで接続をリッスンしています 3333、だからあなたはあなたのリクエストをしたいと思うでしょう localhost 同じポートで:

  1. curl http://localhost:3333

出力は次のようになります。

Output
This is my website!

出力には、 This is my website! からの応答 getRoot あなたがアクセスしたので、関数 / HTTPサーバー上のパス。

次に、同じ端末で、同じホストとポートにリクエストを送信しますが、 /hello あなたの終わりへの道 curl 指図:

  1. curl http://localhost:3333/hello

出力は次のようになります。

Output
Hello, HTTP!

今回は Hello, HTTP! からの応答 getHello 関数。

HTTPサーバー機能を実行している端末を参照すると、サーバーから2行の出力が得られます。 1つは / リクエストと別の /hello リクエスト:

Output
got / request got /hello request

サーバーはプログラムの実行が終了するまで実行を継続するため、サーバーを自分で停止する必要があります。 これを行うには、を押します CONTROL+C プログラムに割り込み信号を送信して停止します。

このセクションでは、HTTPサーバープログラムを作成しましたが、デフォルトのサーバーマルチプレクサとデフォルトのHTTPサーバーを使用しています。 デフォルトまたはグローバルの値を使用すると、プログラムの複数の部分が異なる時間にそれらを更新する可能性があるため、複製が難しいバグが発生する可能性があります。 これが誤った状態につながる場合、特定の関数が特定の順序で呼び出された場合にのみ存在する可能性があるため、バグを追跡するのは難しい場合があります。 したがって、この問題を回避するには、次のセクションで自分で作成したサーバーマルチプレクサを使用するようにサーバーを更新します。

多重化要求ハンドラー

前のセクションでHTTPサーバーを起動したときに、 ListenAndServe 機能a nil の値 http.Handler デフォルトのサーバーマルチプレクサを使用していたため、パラメータ。 なぜなら http.Handler インターフェースであり、独自のインターフェースを作成することが可能です struct インターフェイスを実装します。 しかし、時には、基本的なものだけが必要です http.Handler これは、デフォルトのサーバーマルチプレクサのように、特定の要求パスに対して単一の関数を呼び出します。 このセクションでは、サーバーマルチプレクサであるhttp.ServeMuxを使用するようにプログラムを更新します。 http.Handler によって提供される実装 net/http これらの場合に使用できるパッケージ。

The http.ServeMux struct デフォルトのサーバーマルチプレクサと同じように構成できるため、グローバルデフォルトの代わりに独自のサーバーの使用を開始するためにプログラムに加える必要のある更新はそれほど多くありません。 を使用するようにプログラムを更新するには http.ServeMux、あなたの main.go もう一度ファイルして、自分のプログラムを使用するようにプログラムを更新してください http.ServeMux:

main.go
...

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", getRoot)
	mux.HandleFunc("/hello", getHello)

	err := http.ListenAndServe(":3333", mux)
	
	...
}

このアップデートでは、新しいものを作成しました http.ServeMux を使用して http.NewServeMux コンストラクタとそれをに割り当てました mux 変数。 その後、更新するだけで済みます http.HandleFunc を使用するための呼び出し mux を呼び出す代わりに変数 http パッケージ。 最後に、への呼び出しを更新しました http.ListenAndServe それを提供するために http.Handler あなたが作成した(mux)の代わりに nil 価値。

これで、を使用してプログラムを再度実行できます。 go run:

  1. go run main.go

プログラムは前回と同じように実行され続けるため、別の端末でサーバーと対話するにはコマンドを実行する必要があります。 まず、 curl リクエストするには / 再びパス:

  1. curl http://localhost:3333

出力は次のようになります。

Output
This is my website!

この出力は以前と同じであることがわかります。

次に、前と同じコマンドを実行します /hello 道:

  1. curl http://localhost:3333/hello

出力は次のようになります。

Output
Hello, HTTP!

このパスの出力も以前と同じです。

最後に、元の端末を参照すると、両方の出力が表示されます。 //hello 以前と同じようにリクエスト:

Output
got / request got /hello request

プログラムに加えた更新は機能的には同じですが、今回は独自の更新を使用しています http.Handler デフォルトのものの代わりに。

最後に、 CONTROL+C もう一度サーバープログラムを終了します。

一度に複数のサーバーを実行する

あなた自身を使用することに加えて http.Handler、 外出 net/http パッケージでは、デフォルト以外のHTTPサーバーを使用することもできます。 サーバーの実行方法をカスタマイズしたい場合や、同じプログラムで複数のHTTPサーバーを同時に実行したい場合があります。 たとえば、同じプログラムから実行するパブリックWebサイトとプライベート管理者Webサイトがあるとします。 デフォルトのHTTPサーバーは1つしか持てないため、デフォルトのサーバーではこれを行うことはできません。 このセクションでは、プログラムを更新して、によって提供される2つのhttp.Server値を使用します。 net/http このような場合のパッケージ—サーバーをより細かく制御したい場合、または同時に複数のサーバーが必要な場合。

あなたの中で main.go ファイルの場合、を使用して複数のHTTPサーバーをセットアップします http.Server. また、ハンドラー関数を更新して、着信のcontext.Contextにアクセスします。 *http.Request. これにより、リクエストの送信元のサーバーを設定できます。 context.Context 変数なので、ハンドラー関数の出力にサーバーを出力できます。

あなたの main.go 再度ファイルを作成し、次のように更新します。

main.go
package main

import (
	// Note: Also remove the 'os' import.
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
)

const keyServerAddr = "serverAddr"

func getRoot(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	fmt.Printf("%s: got / request\n", ctx.Value(keyServerAddr))
	io.WriteString(w, "This is my website!\n")
}
func getHello(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))
	io.WriteString(w, "Hello, HTTP!\n")
}

上記のコード更新では、 import 更新に必要なパッケージを含めるステートメント。 次に、作成しました const string と呼ばれる値 keyServerAddr でHTTPサーバーのアドレス値のキーとして機能します http.Request 環境。 最後に、両方を更新しました getRootgetHello アクセスする機能 http.Requestcontext.Context 価値。 値を取得したら、HTTPサーバーのアドレスを fmt.Printf 2つのサーバーのどちらがHTTPリクエストを処理したかを確認できるように出力します。

今、あなたの更新を開始します main 2つのうち最初の1つを追加して機能する http.Server 値:

main.go
...
func main() {
	...
	mux.HandleFunc("/hello", getHello)

	ctx, cancelCtx := context.WithCancel(context.Background())
	serverOne := &http.Server{
		Addr:    ":3333",
		Handler: mux,
		BaseContext: func(l net.Listener) context.Context {
			ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
			return ctx
		},
	}

更新されたコードで、最初に行ったことは、新しいコードを作成することです context.Context 利用可能な機能を備えた値、 cancelCtx、コンテキストをキャンセルします。 次に、 serverOne http.Server 価値。 この値は、すでに使用しているHTTPサーバーと非常に似ていますが、アドレスとハンドラーをに渡す代わりに http.ListenAndServe 関数、あなたはそれらをとして設定します AddrHandler の値 http.Server.

もう1つの変更は、 BaseContext 関数。 BaseContext の一部を変更する方法です context.Context ハンドラー関数が呼び出すときに受け取る Context の方法 *http.Request. プログラムの場合、サーバーがリッスンしているアドレスを追加します(l.Addr().String())キーを持つコンテキストに serverAddr、これはハンドラー関数の出力に出力されます。

次に、2番目のサーバーを定義します。 serverTwo:

main.go
...

func main() {
	...
	serverOne := &http.Server {
		...
	}

	serverTwo := &http.Server{
		Addr:    ":4444",
		Handler: mux,
		BaseContext: func(l net.Listener) context.Context {
			ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
			return ctx
		},
	}

このサーバーは、最初のサーバーと同じように定義されていますが、 :3333 のために Addr フィールド、あなたはそれをに設定します :4444. このようにして、1台のサーバーがポートで接続をリッスンします 3333 2番目のサーバーはポートでリッスンします 4444.

次に、プログラムを更新して最初のサーバーを起動します。 serverOne、ゴルーチンで:

main.go
...

func main() {
	...
	serverTwo := &http.Server {
		...
	}

	go func() {
		err := serverOne.ListenAndServe()
		if errors.Is(err, http.ErrServerClosed) {
			fmt.Printf("server one closed\n")
		} else if err != nil {
			fmt.Printf("error listening for server one: %s\n", err)
		}
		cancelCtx()
	}()

ゴルーチン内で、サーバーを次のように起動します ListenAndServe、以前と同じですが、今回は、関数にパラメーターを指定する必要はありません。 http.ListenAndServe なぜなら http.Server 値はすでに構成されています。 次に、前と同じエラー処理を行います。 関数の最後に、 cancelCtx HTTPハンドラーと両方のサーバーに提供されているコンテキストをキャンセルします BaseContext 機能。 このように、サーバーが何らかの理由で終了した場合、コンテキストも終了します。

最後に、プログラムを更新して、ゴルーチンで2番目のサーバーも起動します。

main.go
...

func main() {
	...
	go func() {
		...
	}()
	go func() {
		err := serverTwo.ListenAndServe()
		if errors.Is(err, http.ErrServerClosed) {
			fmt.Printf("server two closed\n")
		} else if err != nil {
			fmt.Printf("error listening for server two: %s\n", err)
		}
		cancelCtx()
	}()

	<-ctx.Done()
}

このゴルーチンは機能的には最初のものと同じで、開始するだけです serverTwo それ以外の serverOne. このアップデートには、 main から読み取る関数 ctx.Done から戻る前のチャネル main 関数。 これにより、いずれかのサーバーゴルーチンが終了し、 cancelCtx と呼ばれます。 コンテキストが終了すると、プログラムは終了します。

完了したら、ファイルを保存して閉じます。

を使用してサーバーを実行します go run 指図:

  1. go run main.go

プログラムは引き続き実行されるため、2番目のターミナルで curl を要求するコマンド / パスと /hello リッスンしているサーバーからのパス 3333、以前のリクエストと同じ:

  1. curl http://localhost:3333
  2. curl http://localhost:3333/hello

出力は次のようになります。

Output
This is my website! Hello, HTTP!

出力には、前に見たのと同じ応答が表示されます。

ここで、同じコマンドをもう一度実行しますが、今回はポートを使用します 4444、に対応するもの serverTwo あなたのプログラムで:

  1. curl http://localhost:4444
  2. curl http://localhost:4444/hello

出力は次のようになります。

Output
This is my website! Hello, HTTP!

これらのリクエストについては、ポートでのリクエストと同じ出力が表示されます 3333 によって提供されています serverOne.

最後に、サーバーが実行されている元の端末を振り返ります。

Output
[::]:3333: got / request [::]:3333: got /hello request [::]:4444: got / request [::]:4444: got /hello request

出力は前に見たものと似ていますが、今回はリクエストに応答したサーバーを示しています。 最初の2つのリクエストは、ポートでリッスンしているサーバーから送信されたことを示しています 3333 (serverOne)、および次の2つの要求は、ポートでリッスンしているサーバーから送信されました 4444 (serverTwo). これらは、から取得された値です。 BaseContextserverAddr 価値。

また、コンピュータが IPv6 を使用するように設定されているかどうかによって、出力が上記の出力とわずかに異なる場合があります。 そうである場合は、上記と同じ出力が表示されます。 そうでない場合は、表示されます 0.0.0.0 それ以外の [::]. これは、構成されている場合、コンピューターがIPv6を介して自分自身と通信するためです。 [::] のIPv6表記です 0.0.0.0.

完了したら、 CONTROL+C もう一度サーバーを停止します。

このセクションでは、を使用して新しいHTTPサーバープログラムを作成しました http.HandleFunchttp.ListenAndServe デフォルトサーバーを実行および構成します。 次に、を使用するように更新しました http.ServeMux のために http.Handler デフォルトのサーバーマルチプレクサの代わりに。 最後に、使用するプログラムを更新しました http.Server 同じプログラムで複数のHTTPサーバーを実行します。

現在HTTPサーバーを実行していますが、あまりインタラクティブではありません。 応答する新しいパスを追加することはできますが、それを超えてユーザーがパスを操作する方法は実際にはありません。 HTTPプロトコルには、ユーザーがパスを超えてHTTPサーバーと対話できるいくつかの方法が含まれています。 次のセクションでは、プログラムを更新して、最初のクエリ文字列値をサポートします。

リクエストのクエリ文字列の検査

ユーザーがHTTPサーバーから返されるHTTP応答に影響を与えることができる方法の1つは、クエリ文字列を使用することです。 クエリ文字列は、URLの末尾に追加される値のセットです。 それは ? 文字、を使用して追加された値 & 区切り文字として。 クエリ文字列値は、HTTPサーバーが応答として送信する結果をフィルタリングまたはカスタマイズする方法として一般的に使用されます。 たとえば、1つのサーバーが results ユーザーが次のようなものを指定できるようにする値 results=10 結果のリストに10個のアイテムを表示したいと言います。

このセクションでは、 getRoot そのを使用するハンドラー関数 *http.Request クエリ文字列値にアクセスし、それらを出力に出力するための値。

まず、 main.go ファイルを更新し、 getRoot クエリ文字列にアクセスする関数 r.URL.Query 方法。 次に、を更新します main 削除する方法 serverTwo 必要がなくなったため、関連するすべてのコード:

main.go
...

func getRoot(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	hasFirst := r.URL.Query().Has("first")
	first := r.URL.Query().Get("first")
	hasSecond := r.URL.Query().Has("second")
	second := r.URL.Query().Get("second")

	fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s\n",
		ctx.Value(keyServerAddr),
		hasFirst, first,
		hasSecond, second)
	io.WriteString(w, "This is my website!\n")
}
...

の中に getRoot 関数、あなたは使用します r.URL の分野 getRoot*http.Request 要求されているURLに関するプロパティにアクセスします。 次に、 Query の方法 r.URL リクエストのクエリ文字列値にアクセスするためのフィールド。 クエリ文字列値にアクセスしたら、データを操作するために使用できる2つの方法があります。 The Has メソッドは bool クエリ文字列に、指定されたキーを持つ値があるかどうかを指定する値。 first. そうして Get メソッドは string 提供されたキーの値を使用します。

理論的には、いつでも使用できます Get 指定されたキーの実際の値、またはキーが存在しない場合は空の文字列を常に返すため、クエリ文字列値を取得するメソッド。 多くの用途では、これで十分ですが、場合によっては、ユーザーが空の値を提供するか、まったく値を提供しないかの違いを知りたい場合があります。 ユースケースによっては、ユーザーが filter 何の価値もない、または彼らが提供しなかった場合 filter まったく。 彼らが提供した場合 filter あなたがそれを「私に何も見せない」として扱いたいと思うかもしれない何の価値もありませんが、 filter 値は「すべてを見せて」という意味です。 使用する HasGet これら2つのケースの違いがわかります。

あなたの中で getRoot 関数、また、出力を更新して、 HasGet 両方の値 firstsecond クエリ文字列値。

今、あなたの更新 main 1つのサーバーの使用に戻る関数:

main.go
...

func main() {
	...
	mux.HandleFunc("/hello", getHello)
	
	ctx := context.Background()
	server := &http.Server{
		Addr:    ":3333",
		Handler: mux,
		BaseContext: func(l net.Listener) context.Context {
			ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
			return ctx
		},
	}

	err := server.ListenAndServe()
	if errors.Is(err, http.ErrServerClosed) {
		fmt.Printf("server closed\n")
	} else if err != nil {
		fmt.Printf("error listening for server: %s\n", err)
	}
}

の中に main 関数、あなたはへの参照を削除しました serverTwo 実行して移動しました server (以前は serverOne)ゴルーチンから出て、 main あなたが実行していた方法と同様の機能 http.ListenAndServe ついさっき。 に戻すこともできます http.ListenAndServe を使用する代わりに http.Server 再度実行しているサーバーは1つだけなので、 http.Server、将来サーバーに追加のカスタマイズを行う場合は、更新する必要が少なくなります。

変更を保存したら、次を使用します go run プログラムを再度実行するには:

  1. go run main.go

サーバーが再び実行を開始するので、2番目のターミナルに戻って実行します。 curl クエリ文字列を使用したコマンド。 このコマンドでは、URLを一重引用符で囲む必要があります(')、そうでない場合、端末のシェルが解釈する可能性があります & 多くのシェルに含まれている「バックグラウンドでこのコマンドを実行する」機能としてのクエリ文字列内の記号。 URLに、の値を含めます first=1 為に first、 と second= 為に second:

  1. curl 'http://localhost:3333?first=1&second='

出力は次のようになります。

Output
This is my website!

からの出力が表示されます curl コマンドは以前のリクエストから変更されていません。

ただし、サーバープログラムの出力に戻すと、新しい出力にクエリ文字列値が含まれていることがわかります。

Output
[::]:3333: got / request. first(true)=1, second(true)=

の出力 first クエリ文字列の値は Has 返されたメソッド true なぜなら first 価値があり、それも Get の値を返しました 1. の出力 second を示す Has 戻ってきた true なぜなら second 含まれていましたが、 Get メソッドは空の文字列以外を返しませんでした。 追加および削除して、さまざまなリクエストを試すこともできます firstsecond または、異なる値を設定して、それらの関数からの出力がどのように変化するかを確認します。

終了したら、を押します CONTROL+C サーバーを停止します。

このセクションでは、1つだけを使用するようにプログラムを更新しました http.Server 繰り返しますが、読書のサポートも追加しました firstsecond のクエリ文字列からの値 getRoot ハンドラー関数。

ただし、ユーザーがHTTPサーバーに入力を提供する方法は、クエリ文字列を使用することだけではありません。 サーバーにデータを送信するもう1つの一般的な方法は、リクエストの本文にデータを含めることです。 次のセクションでは、プログラムを更新して、からリクエストの本文を読み取ります。 *http.Request データ。

リクエスト本文を読む

REST API などのHTTPベースのAPIを作成する場合、ユーザーはURLの長さの制限に含めることができるよりも多くのデータを送信する必要があるか、ページがフィルタや結果の制限など、データの解釈方法。 このような場合、リクエストの本文にデータを含めて、次のいずれかで送信するのが一般的です。 POST または PUT HTTPリクエスト。

外出先で http.HandlerFunc*http.Request valueは、着信リクエストに関する情報にアクセスするために使用されます。また、 Body 分野。 このセクションでは、 getRoot リクエストの本文を読み取るハンドラー関数。

を更新するには getRoot メソッド、あなたの main.go ファイルを作成し、使用するように更新します ioutil.ReadAll 読むには r.Body リクエストフィールド:

main.go
package main

import (
	...
	"io/ioutil"
	...
)

...

func getRoot(w http.ResponseWriter, r *http.Request) {
	...
	second := r.URL.Query().Get("second")

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		fmt.Printf("could not read body: %s\n", err)
	}

	fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s, body:\n%s\n",
		ctx.Value(keyServerAddr),
		hasFirst, first,
		hasSecond, second,
		body)
	io.WriteString(w, "This is my website!\n")
}

...

このアップデートでは、 ioutil.ReadAll 読み取る関数 r.Body のプロパティ *http.Request リクエストの本文にアクセスします。 The ioutil.ReadAll functionは、エラーまたはデータの終わりが発生するまでio.Readerからデータを読み取るユーティリティ関数です。 以来 r.Bodyio.Reader、あなたはそれを使って体を読むことができます。 本文を読んだら、 fmt.Printf 出力に出力します。

更新を保存したら、を使用してサーバーを実行します go run 指図:

  1. go run main.go

サーバーは停止するまで稼働し続けるので、他の端末に移動して POST を使用してリクエスト curl とともに -X POST オプションとボディを使用して -d オプション。 また、使用することができます firstsecond 以前の文字列値もクエリします。

  1. curl -X POST -d 'This is the body' 'http://localhost:3333?first=1&second='

出力は次のようになります。

Output
This is my website!

ハンドラー関数からの出力は同じですが、サーバーログが再度更新されていることがわかります。

Output
[::]:3333: got / request. first(true)=1, second(true)=, body: This is the body

サーバーログには、以前のクエリ文字列の値が表示されますが、現在は This is the body データ curl コマンドが送信されました。

次に、を押してサーバーを停止します CONTROL+C.

このセクションでは、プログラムを更新して、リクエストの本文を出力に出力した変数に読み込みました。 この方法で本文を読み取ることと、 encoding / json などの他の機能を組み合わせてJSON本文をGoデータにアンマーシャリングすることで、ユーザーが操作できるAPIを作成できます。他のAPIに精通している。

ただし、ユーザーから送信されるすべてのデータがAPIの形式であるとは限りません。 多くのWebサイトには、ユーザーに入力を求めるフォームがあるため、次のセクションでは、プログラムを更新して、既存のリクエスト本文とクエリ文字列に加えてフォームデータを読み取ります。

フォームデータの取得

長い間、フォームを使用してデータを送信することは、ユーザーがHTTPサーバーにデータを送信してWebサイトと対話するための標準的な方法でした。 フォームは以前ほど人気がありませんが、ユーザーがWebサイトにデータを送信する方法としてはまだ多くの用途があります。 The *http.Request の値 http.HandlerFunc また、クエリ文字列とリクエスト本文へのアクセスを提供するのと同様に、このデータにアクセスする方法も提供します。 このセクションでは、 getHello フォームからユーザーの名前を受け取り、ユーザーの名前で返信するプログラム。

あなたの main.go を更新します getHello 使用する関数 PostFormValue の方法 *http.Request:

main.go
...

func getHello(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))

	myName := r.PostFormValue("myName")
	if myName == "" {
		myName = "HTTP"
	}
	io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
}

...

今あなたの getHello 関数、ハンドラー関数に投稿されたフォーム値を読み取り、という名前の値を探しています myName. 値が見つからない場合、または見つかった値が空の文字列である場合は、 myName 変数をデフォルト値に HTTP そのため、ページには空の名前は表示されません。 次に、ユーザーへの出力を更新して、ユーザーが送信した名前を表示するか、 HTTP 彼らが名前を送っていなかったら。

これらの更新を使用してサーバーを実行するには、変更を保存し、を使用して実行します go run:

  1. go run main.go

次に、2番目のターミナルで curl とともに -X POST オプション /hello URLですが、今回は使用する代わりに -d データ本文を提供するには、 -F 'myName=Sammy' フォームデータを提供するオプション myName 値を持つフィールド Sammy:

  1. curl -X POST -F 'myName=Sammy' 'http://localhost:3333/hello'

出力は次のようになります。

Output
Hello, Sammy!

上記の出力では、期待されることがわかります Hello, Sammy! 一緒に送ったフォームだからあいさつ curl 言った myNameSammy.

The r.PostFormValue で使用しているメソッド getHello を取得する関数 myName フォーム値は、リクエストの本文にフォームから投稿された値のみを含む特別なメソッドです。 ただし、 r.FormValue フォーム本体とクエリ文字列の値の両方を含むメソッドも使用できます。 だから、あなたが使用した場合 r.FormValue("myName") 削除することもできます -F オプションとインクルード myName=Sammy クエリ文字列で表示する Sammy 同様に戻った。 に変更せずにそれを行った場合 r.FormValueただし、デフォルトが表示されます HTTP 名前に対する応答。 これらの値を取得する場所に注意することで、名前の競合や追跡が難しいバグを回避できます。 より厳密にして、 r.PostFormValue クエリ文字列にも柔軟性を持たせたい場合を除きます。

サーバーログを振り返ると、 /hello リクエストは以前のリクエストと同様にログに記録されました:

Output
[::]:3333: got /hello request

サーバーを停止するには、を押します CONTROL+C.

このセクションでは、 getHello ページに投稿されたフォームデータから名前を読み取り、その名前をユーザーに返すハンドラー関数。

プログラムのこの時点では、リクエストを処理するときにいくつかの問題が発生する可能性があり、ユーザーには通知されません。 次のセクションでは、ハンドラー関数を更新して、HTTPステータスコードとヘッダーを返します。

ヘッダーとステータスコードで応答する

HTTPプロトコルは、ブラウザやサーバーの通信を支援するためにデータを送信するために、ユーザーが通常は見ないいくつかの機能を使用します。 これらの機能の1つはステータスコードと呼ばれ、サーバーがHTTPクライアントに、サーバーがリクエストを成功と見なしたかどうか、またはサーバー側で問題が発生したかどうかをより正確に把握するために使用されます。またはクライアントが送信したリクエストを使用します。

HTTPサーバーとクライアントが通信するもう1つの方法は、ヘッダーフィールドを使用することです。 ヘッダーフィールドは、クライアントまたはサーバーのいずれかが相手に自分自身について知らせるために送信するキーと値です。 HTTPプロトコルによって事前定義されているヘッダーは多数あります。 Accept、クライアントがサーバーに受け入れて理解できるデータのタイプを伝えるために使用します。 接頭辞を付けて独自に定義することもできます x- その後、名前の残りの部分。

このセクションでは、プログラムを更新して、 myName のフォームフィールド getHello、必須フィールド。 値が送信されない場合 myName フィールドに、サーバーは「Bad Request」ステータスコードをクライアントに送り返し、 x-missing-field 欠落しているフィールドをクライアントに通知するヘッダー。

この機能をプログラムに追加するには、 main.go 最後にもう一度ファイルし、検証チェックをに追加します getHello ハンドラー関数:

main.go
...

func getHello(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))

	myName := r.PostFormValue("myName")
	if myName == "" {
		w.Header().Set("x-missing-field", "myName")
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
}

...

このアップデートでは、 myName のデフォルト名を設定する代わりに、空の文字列です HTTP、代わりにクライアントにエラーメッセージを送信します。 まず、 w.Header().Set を設定する方法 x-missing-field 値が myName 応答HTTPヘッダー内。 次に、 w.WriteHeader 応答ヘッダーと「BadRequest」ステータスコードをクライアントに書き込むメソッド。 最後に、ハンドラー関数から戻ります。 誤って書いたりしないように、これを確実に実行する必要があります。 Hello, ! エラー情報に加えて、クライアントへの応答。

ヘッダーを設定し、ステータスコードを正しい順序で送信していることを確認することも重要です。 HTTPリクエストまたはレスポンスでは、本文がクライアントに送信される前にすべてのヘッダーを送信する必要があるため、更新するリクエストはすべて w.Header() 前に行う必要があります w.WriteHeader と呼ばれます。 一度 w.WriteHeader が呼び出されると、ページのステータスがすべてのヘッダーとともに送信され、後に本文のみを書き込むことができます。

更新を保存したら、次のコマンドを使用してプログラムを再度実行できます。 go run 指図:

  1. go run main.go

次に、2番目の端末を使用して別の端末を作成します curl -X POST にリクエスト /hello URLですが、 -F フォームデータを送信します。 また、 -v 伝えるオプション curl リクエストのすべてのヘッダーと出力を確認できるように、詳細な出力を表示するには、次のようにします。

  1. curl -v -X POST 'http://localhost:3333/hello'

今回の出力では、詳細な出力のためにリクエストが処理されるときに、より多くの情報が表示されます。

Output
* Trying ::1:3333... * Connected to localhost (::1) port 3333 (#0) > POST /hello HTTP/1.1 > Host: localhost:3333 > User-Agent: curl/7.77.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 400 Bad Request < X-Missing-Field: myName < Date: Wed, 02 Mar 2022 03:51:54 GMT < Content-Length: 0 < * Connection #0 to host localhost left intact

出力の最初の数行は、 curl 接続しようとしています localhost ポート 3333.

次に、で始まる行 > リクエストを表示 curl サーバーに作成しています。 それは言う curl 作っている POST にリクエスト /hello HTTP 1.1プロトコルを使用するURL、およびその他のいくつかのヘッダー。 次に、空で示されるように空のボディを送信します > ライン。

一度 curl リクエストを送信すると、サーバーから受信したレスポンスを確認できます。 < プレフィックス。 最初の行は、サーバーが応答したことを示しています Bad Request、これは400ステータスコードとも呼ばれます。 次に、あなたは見ることができます X-Missing-Field 設定したヘッダーは、の値に含まれます myName. いくつかの追加ヘッダーを送信した後、リクエストは本文を送信せずに終了します。これは、 Content-Length (または体)長さ 0.

サーバーの出力をもう一度振り返ると、 /hello 出力で処理されるサーバーを要求します。

Output
[::]:3333: got /hello request

最後にもう一度、を押します CONTROL+C サーバーを停止します。

このセクションでは、HTTPサーバーを更新して、検証を追加しました。 /hello フォーム入力。 フォームの一部として名前が送信されない場合は、 w.Header().Set クライアントに送り返すヘッダーを設定します。 ヘッダーが設定されると、 w.WriteHeader クライアントにヘッダーを書き込み、クライアントにそれが悪い要求であることを示すステータスコードを書き込みます。

結論

このチュートリアルでは、を使用して新しいGoHTTPサーバーを作成しました。 net/http Goの標準ライブラリのパッケージ。 次に、特定のサーバーマルチプレクサと複数のマルチプレクサを使用するようにプログラムを更新しました http.Server インスタンス。 また、クエリ文字列値、リクエスト本文、フォームデータを介してユーザー入力を読み取るようにサーバーを更新しました。 最後に、カスタムHTTPヘッダーと「BadRequest」ステータスコードを使用してフォーム検証情報をクライアントに返すようにサーバーを更新しました。

Go HTTPエコシステムの良い点の1つは、多くのフレームワークがGoにきちんと統合されるように設計されていることです。 net/http すでに存在する多くのコードを再発明する代わりに、パッケージ。 github.com/go-chi/chiプロジェクトはこの良い例です。 Goに組み込まれているサーバーマルチプレクサはHTTPサーバーを使い始めるのに良い方法ですが、より大きなWebサーバーが必要とする可能性のある多くの高度な機能が欠けています。 などのプロジェクト chi を実装することができます http.Handler 標準にぴったり合うようにGo標準ライブラリのインターフェース http.Server コードのサーバー部分を書き直す必要はありません。 これにより、基本的な機能に取り組む代わりに、ミドルウェアやその他のツールの作成に集中して、利用可能なものを強化することができます。

のようなプロジェクトに加えて chi、Go net / http パッケージには、このチュートリアルでカバーされていない多くの機能も含まれています。 Cookieの操作またはHTTPSトラフィックの提供について詳しくは、 net/http パッケージは始めるのに良い場所です。

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