GoでHTTPサーバーを作成する方法
序章
多くの開発者は、インターネット上でコンテンツを配布するためのサーバーの作成に少なくとも一部の時間を費やしています。 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
ディレクトリとそれに移動します:
- 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
ファイル、2つの関数を作成します、 getRoot
と getHello
、ハンドラー関数として機能します。 次に、を作成します main
関数を使用して、リクエストハンドラーを設定します。 http.HandleFunc
それを渡すことによって機能 /
のパス getRoot
ハンドラー関数と /hello
のパス getHello
ハンドラー関数。 ハンドラーを設定したら、 http.ListenAndServe
サーバーを起動してリクエストをリッスンする機能。
次のコードをファイルに追加して、プログラムを開始し、ハンドラーを設定します。
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.ResponseWriter
はio.Writerです。これは、そのインターフェイスに書き込むことができるものなら何でも使用して、応答本文に書き込むことができることを意味します。 この場合、 io.WriteString 関数を使用して、本文への応答を書き込みます。
今、あなたのを開始することによってあなたのプログラムを作成し続けます main
関数:
...
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
示されているように機能します:
...
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プログラムとは異なり、このプログラムはそれ自体ですぐに終了することはありません。 プログラムを実行したら、次のコマンドに進みます。
- go run main.go
プログラムはまだターミナルで実行されているため、サーバーと対話するには2番目のターミナルを開く必要があります。 以下のコマンドと同じ色のコマンドまたは出力が表示されている場合は、この2番目の端末で実行することを意味します。
この2番目の端末では、 curl プログラムを使用して、HTTPサーバーにHTTPリクエストを送信します。 curl
は、さまざまなタイプのサーバーに要求を行うことができる多くのシステムにデフォルトで一般的にインストールされるユーティリティです。 このチュートリアルでは、これを使用してHTTPリクエストを作成します。 サーバーはコンピューターのポートで接続をリッスンしています 3333
、だからあなたはあなたのリクエストをしたいと思うでしょう localhost
同じポートで:
- curl http://localhost:3333
出力は次のようになります。
OutputThis is my website!
出力には、 This is my website!
からの応答 getRoot
あなたがアクセスしたので、関数 /
HTTPサーバー上のパス。
次に、同じ端末で、同じホストとポートにリクエストを送信しますが、 /hello
あなたの終わりへの道 curl
指図:
- curl http://localhost:3333/hello
出力は次のようになります。
OutputHello, HTTP!
今回は Hello, HTTP!
からの応答 getHello
関数。
HTTPサーバー機能を実行している端末を参照すると、サーバーから2行の出力が得られます。 1つは /
リクエストと別の /hello
リクエスト:
Outputgot / 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
:
...
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
:
- 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
、 外出 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
再度ファイルを作成し、次のように更新します。
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
環境。 最後に、両方を更新しました getRoot
と getHello
アクセスする機能 http.Request
の context.Context
価値。 値を取得したら、HTTPサーバーのアドレスを fmt.Printf
2つのサーバーのどちらがHTTPリクエストを処理したかを確認できるように出力します。
今、あなたの更新を開始します main
2つのうち最初の1つを追加して機能する http.Server
値:
...
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
関数、あなたはそれらをとして設定します Addr
と Handler
の値 http.Server
.
もう1つの変更は、 BaseContext
関数。 BaseContext
の一部を変更する方法です context.Context
ハンドラー関数が呼び出すときに受け取る Context
の方法 *http.Request
. プログラムの場合、サーバーがリッスンしているアドレスを追加します(l.Addr().String()
)キーを持つコンテキストに serverAddr
、これはハンドラー関数の出力に出力されます。
次に、2番目のサーバーを定義します。 serverTwo
:
...
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
、ゴルーチンで:
...
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番目のサーバーも起動します。
...
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
指図:
- go run main.go
プログラムは引き続き実行されるため、2番目のターミナルで curl
を要求するコマンド /
パスと /hello
リッスンしているサーバーからのパス 3333
、以前のリクエストと同じ:
- curl http://localhost:3333
- curl http://localhost:3333/hello
出力は次のようになります。
OutputThis is my website!
Hello, HTTP!
出力には、前に見たのと同じ応答が表示されます。
ここで、同じコマンドをもう一度実行しますが、今回はポートを使用します 4444
、に対応するもの serverTwo
あなたのプログラムで:
- curl http://localhost:4444
- curl http://localhost:4444/hello
出力は次のようになります。
OutputThis 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
). これらは、から取得された値です。 BaseContext
の serverAddr
価値。
また、コンピュータが IPv6 を使用するように設定されているかどうかによって、出力が上記の出力とわずかに異なる場合があります。 そうである場合は、上記と同じ出力が表示されます。 そうでない場合は、表示されます 0.0.0.0
それ以外の [::]
. これは、構成されている場合、コンピューターがIPv6を介して自分自身と通信するためです。 [::]
のIPv6表記です 0.0.0.0
.
完了したら、 CONTROL+C
もう一度サーバーを停止します。
このセクションでは、を使用して新しいHTTPサーバープログラムを作成しました http.HandleFunc
と http.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
必要がなくなったため、関連するすべてのコード:
...
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
値は「すべてを見せて」という意味です。 使用する Has
と Get
これら2つのケースの違いがわかります。
あなたの中で getRoot
関数、また、出力を更新して、 Has
と Get
両方の値 first
と second
クエリ文字列値。
今、あなたの更新 main
1つのサーバーの使用に戻る関数:
...
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
プログラムを再度実行するには:
- go run main.go
サーバーが再び実行を開始するので、2番目のターミナルに戻って実行します。 curl
クエリ文字列を使用したコマンド。 このコマンドでは、URLを一重引用符で囲む必要があります('
)、そうでない場合、端末のシェルが解釈する可能性があります &
多くのシェルに含まれている「バックグラウンドでこのコマンドを実行する」機能としてのクエリ文字列内の記号。 URLに、の値を含めます first=1
為に first
、 と second=
為に second
:
- curl 'http://localhost:3333?first=1&second='
出力は次のようになります。
OutputThis is my website!
からの出力が表示されます curl
コマンドは以前のリクエストから変更されていません。
ただし、サーバープログラムの出力に戻すと、新しい出力にクエリ文字列値が含まれていることがわかります。
Output[::]:3333: got / request. first(true)=1, second(true)=
の出力 first
クエリ文字列の値は Has
返されたメソッド true
なぜなら first
価値があり、それも Get
の値を返しました 1
. の出力 second
を示す Has
戻ってきた true
なぜなら second
含まれていましたが、 Get
メソッドは空の文字列以外を返しませんでした。 追加および削除して、さまざまなリクエストを試すこともできます first
と second
または、異なる値を設定して、それらの関数からの出力がどのように変化するかを確認します。
終了したら、を押します CONTROL+C
サーバーを停止します。
このセクションでは、1つだけを使用するようにプログラムを更新しました http.Server
繰り返しますが、読書のサポートも追加しました first
と second
のクエリ文字列からの値 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
リクエストフィールド:
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.Body
は io.Reader
、あなたはそれを使って体を読むことができます。 本文を読んだら、 fmt.Printf
出力に出力します。
更新を保存したら、を使用してサーバーを実行します go run
指図:
- go run main.go
サーバーは停止するまで稼働し続けるので、他の端末に移動して POST
を使用してリクエスト 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
サーバーログには、以前のクエリ文字列の値が表示されますが、現在は 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
:
...
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'
フォームデータを提供するオプション myName
値を持つフィールド Sammy
:
- curl -X POST -F 'myName=Sammy' 'http://localhost:3333/hello'
出力は次のようになります。
OutputHello, Sammy!
上記の出力では、期待されることがわかります Hello, Sammy!
一緒に送ったフォームだからあいさつ curl
言った myName
は Sammy
.
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
ハンドラー関数:
...
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
指図:
- go run main.go
次に、2番目の端末を使用して別の端末を作成します curl -X POST
にリクエスト /hello
URLですが、 -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
作っている 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に関する多くのトピックを取り上げています。