GoでHTTPサーバーを作成する方法

提供:Dev Guides
移動先:案内検索

著者は、 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ファイルでは、ハンドラー関数として機能するgetRootgetHelloの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.ResponseWriterio.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.Handlermux)を提供しました。

これで、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.Requestcontext.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サーバーのアドレス値のキーとして機能するconststring値を作成しました。 最後に、getRoot関数とgetHello関数の両方を更新して、http.Requestcontext.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値を作成することです。 次に、serverOnehttp.Serverの値を定義します。 この値は、すでに使用しているHTTPサーバーと非常に似ていますが、アドレスとハンドラーをhttp.ListenAndServe関数に渡す代わりに、AddrHandler値。

もう1つの変更は、BaseContext関数の追加です。 BaseContextは、ハンドラー関数が*http.RequestContextメソッドを呼び出すときに受け取る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つのリクエストは、ポート3333serverOne)でリッスンしているサーバーからのものであり、次の2つのリクエストは、ポート4444serverTwo)。 これらは、BaseContextserverAddr値から取得された値です。

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

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

このセクションでは、http.HandleFuncおよびhttp.ListenAndServeを使用して新しいHTTPサーバープログラムを作成し、デフォルトサーバーを実行および構成しました。 次に、デフォルトのサーバーマルチプレクサの代わりにhttp.Handlerhttp.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.Requestr.URLフィールドを使用して、要求されているURLに関するプロパティにアクセスします。 次に、r.URLフィールドのQueryメソッドを使用して、要求のクエリ文字列値にアクセスします。 クエリ文字列値にアクセスしたら、データを操作するために使用できる2つの方法があります。 Hasメソッドは、クエリ文字列にfirstなどの指定されたキーを持つ値があるかどうかを指定するbool値を返します。 次に、Getメソッドは、指定されたキーの値を含むstringを返します。

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

getRoot関数で、firstsecondの両方のクエリ文字列値のHasGetの値を表示するように出力も更新しました。

次に、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が含まれているため、Hastrueを返したが、Getメソッドはそれ以外を返さなかったことを示しています。空の文字列。 firstsecondを追加および削除するか、異なる値を設定して、これらの関数からの出力がどのように変化するかを確認することで、異なる要求を行うこともできます。

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

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

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

リクエスト本文を読む

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

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.Requestr.Bodyプロパティを読み取り、リクエストの本文にアクセスします。 ioutil.ReadAll関数は、エラーまたはデータの終わりが発生するまで、io.Readerからデータを読み取るユーティリティ関数です。 r.Bodyio.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.RequestPostFormValueメソッドを使用します。

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'オプションを使用して、値SammymyNameフィールドを持つフォームデータを提供します。

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

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

OutputHello, Sammy!

上記の出力では、curlで送信したフォームにmyNameSammyであるため、予想される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

出力の最初の数行は、curllocalhostポート3333に接続しようとしていることを示しています。

次に、>で始まる行は、curlがサーバーに対して行っている要求を示しています。 curlがHTTP1.1プロトコルと他のいくつかのヘッダーを使用して/helloURLに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に関する多くのトピックを取り上げています。