GoでHTTPサーバーを作成する方法
著者は、 Diversity in Tech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
多くの開発者は、インターネット上でコンテンツを配布するためのサーバーの作成に少なくとも一部の時間を費やしています。 Hypertext Transfer Protocol (HTTP)は、猫の画像のリクエストであれ、現在読んでいるチュートリアルの読み込みのリクエストであれ、このコンテンツの多くを提供します。 Go標準ライブラリは、Webコンテンツを提供するHTTPサーバーを作成したり、それらのサーバーにHTTPリクエストを送信したりするための組み込みサポートを提供します。
このチュートリアルでは、Goの標準ライブラリを使用してHTTPサーバーを作成し、サーバーを拡張して、リクエストのクエリ文字列、本文、フォームデータからデータを読み取ります。 また、独自のHTTPヘッダーとステータスコードを使用してリクエストに応答するようにプログラムを更新します。
前提条件
このチュートリアルに従うには、次のものが必要です。
- Go version 1.16 or greater installed. To set this up, follow the How To Install Go tutorial for your operating system.
- curlを使用してWebリクエストを行う機能。 curlについて詳しくは、cURLを使用してファイルをダウンロードする方法をご覧ください。
- GoでJSONを使用する方法チュートリアルにある、GoでのJSONの使用に関する知識。
- Goの
context
パッケージの経験。これは、チュートリアルGoでコンテキストを使用する方法で取得できます。 - チュートリアルGoで複数の関数を同時に実行する方法から得られるゴルーチンの実行とチャネルの読み取りの経験。
- HTTP リクエストの作成方法と送信方法に精通していること(推奨)。
プロジェクトの設定
Goでは、ほとんどのHTTP機能は標準ライブラリの net / http パッケージによって提供され、残りのネットワーク通信はnetパッケージによって提供されます。 net/http
パッケージには、HTTPリクエストを作成する機能が含まれているだけでなく、それらのリクエストを処理するために使用できるHTTPサーバーも提供されます。
このセクションでは、 http.ListenAndServe 関数を使用して、要求パス/
および/hello
に応答するHTTPサーバーを起動するプログラムを作成します。 次に、そのプログラムを拡張して、同じプログラムで複数のHTTPサーバーを実行します。
ただし、コードを作成する前に、プログラムのディレクトリを作成する必要があります。 多くの開発者は、プロジェクトを整理するためにディレクトリに保管しています。 このチュートリアルでは、projects
という名前のディレクトリを使用します。
まず、projects
ディレクトリを作成し、次の場所に移動します。
mkdir projects cd projects
次に、プロジェクトのディレクトリを作成し、そのディレクトリに移動します。 この場合、ディレクトリhttpserver
を使用します。
mkdir httpserver cd httpserver
プログラムのディレクトリが作成され、httpserver
ディレクトリに移動したので、HTTPサーバーの実装を開始できます。
リクエストのリッスンと応答の提供
Go HTTPサーバーには、2つの主要なコンポーネントが含まれています。HTTPクライアントからの要求をリッスンするサーバーと、それらの要求に応答する1つ以上の要求ハンドラーです。 このセクションでは、まず関数http.HandleFunc
を使用して、サーバーへの要求を処理するために呼び出す関数をサーバーに指示します。 次に、http.ListenAndServe
関数を使用してサーバーを起動し、新しいHTTP要求をリッスンし、設定したハンドラー関数を使用してそれらを処理するようにサーバーに指示します。
次に、作成したhttpserver
ディレクトリ内で、nano
またはお気に入りのエディタを使用して、main.go
ファイルを開きます。
nano main.go
main.go
ファイルでは、ハンドラー関数として機能するgetRoot
とgetHello
の2つの関数を作成します。 次に、main
関数を作成し、それを使用して、getRoot
の/
パスを渡すことにより、http.HandleFunc
関数でリクエストハンドラーを設定します。 ]ハンドラー関数とgetHello
ハンドラー関数の/hello
パス。 ハンドラーを設定したら、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") }
この最初のコードチャンクでは、Goプログラムにpackage
、プログラムに必要なパッケージimport
を設定し、getRoot
関数の2つの関数を作成します。 getHello
機能。 これらの関数は両方とも同じ関数シグネチャを持ち、同じ引数(http.ResponseWriter
値と*http.Request
値)を受け入れます。 この関数シグネチャはHTTPハンドラー関数に使用され、http.HandlerFuncとして定義されます。 サーバーに対して要求が行われると、サーバーは、行われた要求に関する情報を使用してこれら2つの値を設定し、それらの値を使用してハンドラー関数を呼び出します。
http.HandlerFunc
では、 http.ResponseWriter 値(ハンドラーではw
という名前)を使用して、要求を行ったクライアントに書き戻される応答情報を制御します。応答の本文やステータスコードなど。 次に、 * http.Request 値(ハンドラーではr
という名前)を使用して、サーバーに入ってきたリクエストに関する情報を取得します。 POST
リクエストまたはリクエストを行ったクライアントに関する情報。
今のところ、両方のHTTPハンドラーで、fmt.Printf
を使用してハンドラー関数の要求が着信したときに出力し、次にhttp.ResponseWriter
を使用してテキストを応答本文に送信します。 http.ResponseWriter
はio.Writerです。つまり、そのインターフェイスに書き込むことができるものなら何でも使用して、応答本文に書き込むことができます。 この場合、 io.WriteString 関数を使用して、本文への応答を書き込みます。
次に、main
関数を開始して、プログラムの作成を続けます。
main.go
... func main() { http.HandleFunc("/", getRoot) http.HandleFunc("/hello", getHello) err := http.ListenAndServe(":3333", nil) ...
main
関数では、http.HandleFunc
関数を2回呼び出す必要があります。 関数を呼び出すたびに、デフォルトのサーバーマルチプレクサで特定の要求パスのハンドラ関数が設定されます。 サーバーマルチプレクサはhttp.Handlerであり、要求パスを調べて、そのパスに関連付けられた特定のハンドラ関数を呼び出すことができます。 したがって、プログラムでは、デフォルトのサーバーマルチプレクサに、誰かが/
パスを要求したときにgetRoot
関数を呼び出し、誰かが/hello
パス。
ハンドラーを設定したら、http.ListenAndServe
関数を呼び出します。この関数は、オプションのhttp.Handler
を使用して、特定のポートで着信要求をリッスンするようにグローバルHTTPサーバーに指示します。 プログラムでは、サーバーに":3333"
をリッスンするように指示します。 コロンの前にIPアドレスを指定しないことにより、サーバーはコンピューターに関連付けられているすべてのIPアドレスをリッスンし、ポート3333
をリッスンします。 ここでの3333
などのネットワークポートは、1台のコンピューターが同時に多数のプログラムを相互に通信させるための方法です。 各プログラムは独自のポートを使用するため、クライアントが特定のポートに接続すると、コンピューターはそれを送信するプログラムを認識します。 IPアドレス127.0.0.1
のホスト名であるlocalhost
への接続のみを許可する場合は、代わりに127.0.0.1:3333
と言うことができます。
http.ListenAndServe
関数は、http.Handler
パラメーターのnil
値も渡します。 これは、ListenAndServe
関数に、設定したものではなく、デフォルトのサーバーマルチプレクサを使用することを通知します。
ListenAndServe
はブロッキング呼び出しです。つまり、ListenAndServe
の実行が終了するまで、プログラムは実行を継続しません。 ただし、ListenAndServe
は、プログラムの実行が終了するか、HTTPサーバーがシャットダウンするように指示されるまで実行を終了しません。 ListenAndServe
がブロックされており、プログラムにサーバーをシャットダウンする方法が含まれていない場合でも、ListenAndServe
の呼び出しが失敗する可能性があるため、エラー処理を含めることが重要です。 したがって、次のように、main
関数のListenAndServe
にエラー処理を追加します。
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番目のエラーチェックでは、他のエラーをチェックします。 この場合、エラーが画面に出力され、os.Exit
関数を使用してエラーコード1
でプログラムが終了します。
プログラムの実行中に表示される可能性のあるエラーの1つは、address already in use
エラーです。 このエラーは、別のプログラムがすでに使用しているために、ListenAndServe
が指定されたアドレスまたはポートでリッスンできない場合に返される可能性があります。 これは、ポートが一般的に使用され、コンピューター上の別のプログラムがそれを使用している場合に発生することがありますが、独自のプログラムの複数のコピーを複数回実行した場合にも発生する可能性があります。 このチュートリアルの作業中にこのエラーが表示された場合は、プログラムを再度実行する前に、前の手順でプログラムを停止したことを確認してください。
注: address already in use
エラーが表示され、プログラムの別のコピーが実行されていない場合は、他のプログラムがそれを使用している可能性があります。 この場合、このチュートリアルで説明されている3333
が表示されている場合は、3334
など、1024より上で65535より下の別の番号に変更して再試行してください。 それでもエラーが表示される場合は、使用されていないポートを探し続ける必要があるかもしれません。 動作するポートを見つけたら、このチュートリアルのすべてのコマンドにそれを使用します。
コードの準備ができたので、main.go
ファイルを保存し、go run
を使用してプログラムを実行します。 あなたが書いたかもしれない他のGoプログラムとは異なり、このプログラムはそれ自体ですぐに終了することはありません。 プログラムを実行したら、次のコマンドに進みます。
go run main.go
プログラムはまだターミナルで実行されているため、サーバーと対話するには2番目のターミナルを開く必要があります。 以下のコマンドと同じ色のコマンドまたは出力が表示されている場合は、この2番目の端末で実行することを意味します。
この2番目の端末では、 curl プログラムを使用して、HTTPサーバーにHTTPリクエストを送信します。 curl
は、さまざまなタイプのサーバーにリクエストを送信できる多くのシステムにデフォルトで一般的にインストールされているユーティリティです。 このチュートリアルでは、これを使用してHTTPリクエストを作成します。 サーバーはコンピューターのポート3333
で接続をリッスンしているため、同じポートでlocalhost
にリクエストを送信する必要があります。
curl http://localhost:3333
出力は次のようになります。
OutputThis is my website!
HTTPサーバーの/
パスにアクセスしたため、出力にはgetRoot
関数からのThis is my website!
応答が表示されます。
次に、同じ端末で同じホストとポートにリクエストを送信しますが、curl
コマンドの最後に/hello
パスを追加します。
curl http://localhost:3333/hello
出力は次のようになります。
OutputHello, HTTP!
今回は、getHello
関数からのHello, HTTP!
応答が表示されます。
HTTPサーバー機能を実行している端末を参照すると、サーバーから2行の出力が得られます。 1つは/
リクエスト用で、もう1つは/hello
リクエスト用です。
Outputgot / request got /hello request
サーバーはプログラムの実行が終了するまで実行を継続するため、サーバーを自分で停止する必要があります。 これを行うには、CONTROL+C
を押してプログラムに割り込み信号を送信し、プログラムを停止します。
このセクションでは、HTTPサーバープログラムを作成しましたが、デフォルトのサーバーマルチプレクサとデフォルトのHTTPサーバーを使用しています。 デフォルトまたはグローバルの値を使用すると、プログラムの複数の部分が異なる時間にそれらを更新する可能性があるため、複製が難しいバグが発生する可能性があります。 これが誤った状態につながる場合、特定の関数が特定の順序で呼び出された場合にのみ存在する可能性があるため、バグを追跡するのは難しい場合があります。 したがって、この問題を回避するには、次のセクションで自分で作成したサーバーマルチプレクサを使用するようにサーバーを更新します。
多重化要求ハンドラー
前のセクションでHTTPサーバーを起動したとき、デフォルトのサーバーマルチプレクサを使用していたため、ListenAndServe
関数にhttp.Handler
パラメータのnil
値を渡しました。 http.Handler
はインターフェースであるため、インターフェースを実装する独自のstruct
を作成することができます。 ただし、デフォルトのサーバーマルチプレクサのように、特定の要求パスに対して単一の関数を呼び出す基本的なhttp.Handler
のみが必要な場合もあります。 このセクションでは、 http.ServeMux 、サーバーマルチプレクサ、およびnet/http
パッケージによって提供されるhttp.Handler
実装を使用するようにプログラムを更新します。これらは、これらの場合に使用できます。 。
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.NewServeMux
コンストラクターを使用して新しいhttp.ServeMux
を作成し、それをmux
変数に割り当てました。 その後、http
パッケージを呼び出す代わりに、mux
変数を使用するようにhttp.HandleFunc
呼び出しを更新するだけで済みました。 最後に、http.ListenAndServe
の呼び出しを更新して、nil
値の代わりに、作成したhttp.Handler
(mux
)を提供しました。
これで、go run
を使用してプログラムを再度実行できます。
go run main.go
プログラムは前回と同じように実行され続けるため、別の端末でサーバーと対話するにはコマンドを実行する必要があります。 まず、curl
を使用して、/
パスを再度要求します。
curl http://localhost:3333
出力は次のようになります。
OutputThis is my website!
この出力は以前と同じであることがわかります。
次に、/hello
パスに対して以前と同じコマンドを実行します。
curl http://localhost:3333/hello
出力は次のようになります。
OutputHello, HTTP!
このパスの出力も以前と同じです。
最後に、元の端末を参照すると、以前と同様に/
と/hello
の両方のリクエストの出力が表示されます。
Outputgot / request got /hello request
プログラムに加えた更新は機能的には同じですが、今回はデフォルトのhttp.Handler
を使用しています。
最後に、もう一度CONTROL+C
を押して、サーバープログラムを終了します。
一度に複数のサーバーを実行する
独自のhttp.Handler
を使用することに加えて、Go net/http
パッケージでは、デフォルト以外のHTTPサーバーを使用することもできます。 サーバーの実行方法をカスタマイズしたい場合や、同じプログラムで複数のHTTPサーバーを同時に実行したい場合があります。 たとえば、同じプログラムから実行するパブリックWebサイトとプライベート管理者Webサイトがあるとします。 デフォルトのHTTPサーバーは1つしか持てないため、デフォルトのサーバーではこれを行うことはできません。 このセクションでは、このような場合にnet/http
パッケージによって提供される2つのhttp.Server 値を使用するようにプログラムを更新します。サーバーをより細かく制御したい場合、またはで複数のサーバーが必要な場合同時に。
main.go
ファイルで、http.Server
を使用して複数のHTTPサーバーをセットアップします。 また、ハンドラー関数を更新して、着信*http.Request
のcontext.Contextにアクセスします。 これにより、リクエストの送信元のサーバーを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
ステートメントを更新して、更新に必要なパッケージを含めました。 次に、[X15X]というコンテキストでHTTPサーバーのアドレス値のキーとして機能するconst
string
値を作成しました。 最後に、getRoot
関数とgetHello
関数の両方を更新して、http.Request
のcontext.Context
値にアクセスしました。 値を取得したら、fmt.Printf
出力にHTTPサーバーのアドレスを含めて、2つのサーバーのどちらがHTTP要求を処理したかを確認できるようにします。
次に、2つのhttp.Server
値の最初の値を追加して、main
関数の更新を開始します。
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 }, }
更新されたコードで最初に行ったことは、コンテキストをキャンセルするために、使用可能な関数cancelCtx
を使用して新しいcontext.Context
値を作成することです。 次に、serverOne
http.Server
の値を定義します。 この値は、すでに使用しているHTTPサーバーと非常に似ていますが、アドレスとハンドラーをhttp.ListenAndServe
関数に渡す代わりに、Addr
とHandler
値。
もう1つの変更は、BaseContext
関数の追加です。 BaseContext
は、ハンドラー関数が*http.Request
のContext
メソッドを呼び出すときに受け取るcontext.Context
の一部を変更する方法です。 プログラムの場合、サーバーがリッスンしているアドレス(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 }, }
このサーバーは、Addr
フィールドの:3333
の代わりに、: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
で行ったように、関数にパラメーターを指定する必要はありません。 X195X]の値はすでに構成されています。 次に、前と同じエラー処理を行います。 関数の最後で、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() }
このゴルーチンは機能的には最初のものと同じで、serverOne
の代わりにserverTwo
を開始します。 このアップデートには、main
関数から戻る前にctx.Done
チャネルから読み取ったmain
関数の終了も含まれています。 これにより、いずれかのサーバーgoroutineが終了し、cancelCtx
が呼び出されるまで、プログラムが実行され続けることが保証されます。 コンテキストが終了すると、プログラムは終了します。
完了したら、ファイルを保存して閉じます。
go run
コマンドを使用してサーバーを実行します。
go run main.go
プログラムは引き続き実行されるため、2番目の端末でcurl
コマンドを実行して、3333
をリッスンしているサーバーに/
パスと/hello
パスを要求します。 ]、以前のリクエストと同じ:
curl http://localhost:3333 curl http://localhost:3333/hello
出力は次のようになります。
OutputThis is my website! Hello, HTTP!
出力には、前に見たのと同じ応答が表示されます。
ここで、同じコマンドを再度実行しますが、今回は、プログラムのserverTwo
に対応するポート4444
を使用します。
curl http://localhost:4444 curl http://localhost:4444/hello
出力は次のようになります。
OutputThis is my website! Hello, HTTP!
これらのリクエストについては、serverOne
によって処理されているポート3333
のリクエストと同じ出力が表示されます。
最後に、サーバーが実行されている元の端末を振り返ります。
Output[::]:3333: got / request [::]:3333: got /hello request [::]:4444: got / request [::]:4444: got /hello request
出力は前に見たものと似ていますが、今回はリクエストに応答したサーバーを示しています。 最初の2つのリクエストは、ポート3333
(serverOne
)でリッスンしているサーバーからのものであり、次の2つのリクエストは、ポート4444
(serverTwo
)。 これらは、BaseContext
のserverAddr
値から取得された値です。
また、コンピュータが IPv6 を使用するように設定されているかどうかによって、出力が上記の出力とわずかに異なる場合があります。 そうである場合は、上記と同じ出力が表示されます。 そうでない場合は、[::]
の代わりに0.0.0.0
が表示されます。 この理由は、構成されている場合、コンピューターはIPv6を介してそれ自体と通信し、[::]
は0.0.0.0
のIPv6表記であるためです。
完了したら、CONTROL+C
をもう一度使用してサーバーを停止します。
このセクションでは、http.HandleFunc
およびhttp.ListenAndServe
を使用して新しいHTTPサーバープログラムを作成し、デフォルトサーバーを実行および構成しました。 次に、デフォルトのサーバーマルチプレクサの代わりにhttp.Handler
にhttp.ServeMux
を使用するように更新しました。 最後に、http.Server
を使用して同じプログラムで複数のHTTPサーバーを実行するように、プログラムを更新しました。
現在HTTPサーバーを実行していますが、あまりインタラクティブではありません。 応答する新しいパスを追加することはできますが、それを超えてユーザーがパスを操作する方法は実際にはありません。 HTTPプロトコルには、ユーザーがパスを超えてHTTPサーバーと対話できるいくつかの方法が含まれています。 次のセクションでは、プログラムを更新して、最初のクエリ文字列値をサポートします。
リクエストのクエリ文字列の検査
ユーザーがHTTPサーバーから返されるHTTP応答に影響を与えることができる方法の1つは、クエリ文字列を使用することです。 クエリ文字列は、URLの末尾に追加される値のセットです。 ?
文字で始まり、&
を区切り文字として使用して追加の値が追加されます。 クエリ文字列値は、HTTPサーバーが応答として送信する結果をフィルタリングまたはカスタマイズする方法として一般的に使用されます。 たとえば、あるサーバーが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
関数では、getRoot
の*http.Request
のr.URL
フィールドを使用して、要求されているURLに関するプロパティにアクセスします。 次に、r.URL
フィールドのQuery
メソッドを使用して、要求のクエリ文字列値にアクセスします。 クエリ文字列値にアクセスしたら、データを操作するために使用できる2つの方法があります。 Has
メソッドは、クエリ文字列にfirst
などの指定されたキーを持つ値があるかどうかを指定するbool
値を返します。 次に、Get
メソッドは、指定されたキーの値を含むstring
を返します。
理論的には、Get
メソッドを使用してクエリ文字列値を取得できます。これは、指定されたキーの実際の値、またはキーが存在しない場合は空の文字列を常に返すためです。 多くの用途では、これで十分ですが、場合によっては、ユーザーが空の値を提供するか、まったく値を提供しないかの違いを知りたい場合があります。 ユースケースによっては、ユーザーがfilter
の値を何も提供していないのか、それともfilter
をまったく提供していないのかを知りたい場合があります。 filter
の値が何もない場合は、「何も表示しない」として扱いますが、filter
の値を指定しないと、「すべてを表示する」という意味になります。 Has
とGet
を使用すると、これら2つのケースの違いがわかります。
getRoot
関数で、first
とsecond
の両方のクエリ文字列値のHas
とGet
の値を表示するように出力も更新しました。
次に、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
)の実行をゴルーチンから[に移動しました。 X152X]関数、以前のhttp.ListenAndServe
の実行方法と同様です。 http.Server
の値を使用する代わりに、http.ListenAndServe
に戻すこともできます。これは、サーバーが1つしか実行されていないためですが、http.Server
を使用すると、更新する必要が少なくなります。将来、サーバーに追加のカスタマイズを加えたいと考えていました。
ここで、変更を保存したら、go run
を使用してプログラムを再度実行します。
go run main.go
サーバーが再び実行を開始するため、2番目の端末に戻って、クエリ文字列を使用してcurl
コマンドを実行します。 このコマンドでは、URLを一重引用符('
)で囲む必要があります。そうしないと、端末のシェルがクエリ文字列の&
記号を「バックグラウンドでこのコマンドを実行する」と解釈する可能性があります。 」多くのシェルに含まれる機能。 URLに、first
の場合はfirst=1
の値を、second
の場合はsecond=
の値を含めます。
curl 'http://localhost:3333?first=1&second='
出力は次のようになります。
OutputThis is my website!
curl
コマンドからの出力が以前のリクエストから変更されていないことがわかります。
ただし、サーバープログラムの出力に戻すと、新しい出力にクエリ文字列値が含まれていることがわかります。
Output[::]:3333: got / request. first(true)=1, second(true)=
first
クエリ文字列値の出力は、first
に値があるため、Has
メソッドがtrue
を返し、Get
が返されることを示しています。 1
の値。 second
の出力は、second
が含まれているため、Has
がtrue
を返したが、Get
メソッドはそれ以外を返さなかったことを示しています。空の文字列。 first
とsecond
を追加および削除するか、異なる値を設定して、これらの関数からの出力がどのように変化するかを確認することで、異なる要求を行うこともできます。
終了したら、CONTROL+C
を押してサーバーを停止します。
このセクションでは、http.Server
を1つだけ使用するようにプログラムを更新しましたが、[のクエリ文字列からfirst
とsecond
の値を読み取るためのサポートも追加しました。 X178X]ハンドラー関数。
ただし、ユーザーがHTTPサーバーに入力を提供する方法は、クエリ文字列を使用することだけではありません。 サーバーにデータを送信するもう1つの一般的な方法は、リクエストの本文にデータを含めることです。 次のセクションでは、*http.Request
データからリクエストの本文を読み取るようにプログラムを更新します。
リクエスト本文を読む
REST API などのHTTPベースのAPIを作成する場合、ユーザーはURLの長さの制限に含めることができるよりも多くのデータを送信する必要があるか、ページがフィルタや結果の制限など、データの解釈方法。 このような場合、リクエストの本文にデータを含め、POST
またはPUT
HTTPリクエストのいずれかでデータを送信するのが一般的です。
Go http.HandlerFunc
では、*http.Request
値は着信要求に関する情報にアクセスするために使用され、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
関数を使用して、*http.Request
のr.Body
プロパティを読み取り、リクエストの本文にアクセスします。 ioutil.ReadAll
関数は、エラーまたはデータの終わりが発生するまで、io.Readerからデータを読み取るユーティリティ関数です。 r.Body
はio.Reader
なので、本文の読み取りに使用できます。 本文を読んだら、fmt.Printf
も更新して出力に出力します。
更新を保存したら、go run
コマンドを使用してサーバーを実行します。
go run main.go
サーバーは停止するまで実行を継続するため、他の端末に移動して、curl
と-X POST
オプションを使用し、本体を-d
オプション。 以前のfirst
およびsecond
クエリ文字列値を使用することもできます。
curl -X POST -d 'This is the body' 'http://localhost:3333?first=1&second='
出力は次のようになります。
OutputThis is my website!
ハンドラー関数からの出力は同じですが、サーバーログが再度更新されていることがわかります。
Output[::]:3333: got / request. first(true)=1, second(true)=, body: This is the body
サーバーログには、以前のクエリ文字列値が表示されますが、curl
コマンドが送信したThis is the body
データも表示されます。
次に、CONTROL+C
を押してサーバーを停止します。
このセクションでは、プログラムを更新して、リクエストの本文を出力に出力した変数に読み込みました。 この方法で本文を読み取ることと、 encoding / json などの他の機能を組み合わせてJSON本文をGoデータにアンマーシャリングすることで、ユーザーが操作できるAPIを作成できます。他のAPIに精通している。
ただし、ユーザーから送信されるすべてのデータがAPIの形式であるとは限りません。 多くのWebサイトには、ユーザーに入力を求めるフォームがあるため、次のセクションでは、プログラムを更新して、既存のリクエスト本文とクエリ文字列に加えてフォームデータを読み取ります。
フォームデータの取得
長い間、フォームを使用してデータを送信することは、ユーザーがHTTPサーバーにデータを送信してWebサイトと対話するための標準的な方法でした。 フォームは以前ほど人気がありませんが、ユーザーがWebサイトにデータを送信する方法としてはまだ多くの用途があります。 http.HandlerFunc
の*http.Request
値も、クエリ文字列とリクエスト本文へのアクセスを提供するのと同様に、このデータにアクセスする方法を提供します。 このセクションでは、getHello
プログラムを更新して、フォームからユーザーの名前を受け取り、その名前でユーザーに返信します。
main.go
を開き、getHello
関数を更新して、*http.Request
のPostFormValue
メソッドを使用します。
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
を使用して実行します。
go run main.go
次に、2番目の端末でcurl
を-X POST
オプションとともに/hello
URLに使用しますが、今回は-d
を使用してデータ本文を提供する代わりに、-F 'myName=Sammy'
オプションを使用して、値Sammy
のmyName
フィールドを持つフォームデータを提供します。
curl -X POST -F 'myName=Sammy' 'http://localhost:3333/hello'
出力は次のようになります。
OutputHello, Sammy!
上記の出力では、curl
で送信したフォームにmyName
がSammy
であるため、予想されるHello, Sammy!
の挨拶が表示されます。
getHello
関数でmyName
フォーム値を取得するために使用しているr.PostFormValue
メソッドは、フォームから投稿された値のみをリクエストの本文に含める特別なメソッドです。 。 ただし、フォーム本体とクエリ文字列の値の両方を含む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つの方法は、ヘッダーフィールドを使用することです。 ヘッダーフィールドは、クライアントまたはサーバーのいずれかが相手に自分自身について知らせるために送信するキーと値です。 Accept
など、HTTPプロトコルによって事前定義されたヘッダーは多数あります。これは、クライアントがサーバーに受け入れて理解できるデータの種類を通知するために使用します。 また、接頭辞として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
メソッドを使用して、応答HTTPヘッダーにmyName
の値を持つx-missing-field
ヘッダーを設定します。 次に、w.WriteHeader
メソッドを使用して、応答ヘッダーと「BadRequest」ステータスコードをクライアントに書き込みます。 最後に、ハンドラー関数から戻ります。 エラー情報に加えて、誤ってHello, !
応答をクライアントに書き込まないように、これを確実に実行する必要があります。
ヘッダーを設定し、ステータスコードを正しい順序で送信していることを確認することも重要です。 HTTPリクエストまたはレスポンスでは、本文がクライアントに送信される前にすべてのヘッダーを送信する必要があるため、w.Header()
を更新するリクエストは、w.WriteHeader
が呼び出される前に実行する必要があります。 w.WriteHeader
が呼び出されると、ページのステータスがすべてのヘッダーとともに送信され、本文のみを後に書き込むことができます。
更新を保存したら、go run
コマンドを使用してプログラムを再度実行できます。
go run main.go
次に、2番目の端末を使用して/hello
URLに対して別のcurl -X POST
リクエストを作成しますが、フォームデータを送信するために-F
を含めないでください。 また、-v
オプションを含めて、curl
に詳細な出力を表示するように指示し、リクエストのすべてのヘッダーと出力を表示できるようにすることもできます。
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
がHTTP1.1プロトコルと他のいくつかのヘッダーを使用して/hello
URLにPOST
リクエストを送信していることを示しています。 次に、空の>
行で示されているように、空の本文を送信します。
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
を使用してヘッダーをクライアントに書き込み、ステータスコードを使用してクライアントに不正なリクエストであることを示しました。
結論
このチュートリアルでは、Goの標準ライブラリにあるnet/http
パッケージを使用して新しいGoHTTPサーバーを作成しました。 次に、特定のサーバーマルチプレクサと複数のhttp.Server
インスタンスを使用するようにプログラムを更新しました。 また、クエリ文字列値、リクエスト本文、フォームデータを介してユーザー入力を読み取るようにサーバーを更新しました。 最後に、カスタムHTTPヘッダーと「BadRequest」ステータスコードを使用してフォーム検証情報をクライアントに返すようにサーバーを更新しました。
Go HTTPエコシステムの良い点の1つは、多くのフレームワークが、既存の多くのコードを再発明するのではなく、Goのnet/http
パッケージにきちんと統合するように設計されていることです。 github.com/go-chi/chiプロジェクトはこの良い例です。 Goに組み込まれているサーバーマルチプレクサはHTTPサーバーを使い始めるのに良い方法ですが、より大きなWebサーバーが必要とする可能性のある多くの高度な機能が欠けています。 chi
などのプロジェクトは、コードのサーバー部分を書き直すことなく、標準http.Server
にぴったり合うようにGo標準ライブラリにhttp.Handler
インターフェイスを実装できます。 これにより、基本的な機能に取り組む代わりに、ミドルウェアやその他のツールの作成に集中して、利用可能なものを強化することができます。
chi
のようなプロジェクトに加えて、Go net / http パッケージには、このチュートリアルでカバーされていない多くの機能も含まれています。 Cookieの操作やHTTPSトラフィックの処理について詳しく知るには、net/http
パッケージから始めることをお勧めします。
このチュートリアルは、 DigitalOcean How to Code inGoシリーズの一部でもあります。 このシリーズでは、Goの初めてのインストールから、言語自体の使用方法まで、Goに関する多くのトピックを取り上げています。