はじめに
Garden Night イベントには、受付、地図、スタッフページ、チケット情報など、複数のローカル Web 入口があります。開場前に、担当者は割り当てたすべてのチェックについて正確な結果を必要としています。アクセスを拒否された入口や応答の遅い入口が、レポートから消えてはいけません。
Go のチェック関数を完成させ、実際の JSON レポートを保存します。Go の関数、構造体、スライス、モジュール、基本的なチャネル構文を理解し、Building a Lost and Found API を修了しているか、同等の HTTP 経験があることを前提とします。HTTP クライアントのエラー、有限のタイムアウト、応答リソースを管理する責任は、使う前に説明します。対象はすべて管理されたローカル検証サービスです。これはイベント前の単発チェックであり、インターネットスキャナーや監視サービスではありません。この単元は独立しており、以前の VM のファイルは不要です。読む時間や通常の間違いへの対応を含め、約 55 分を見込んでください。これは著者による見積もりです。
小さな成功例の結果を読む
このステップでは、小さな動作例を実行し、1 件の結果が何を識別するかを学びます。Workspace から始めてください。Go のソースとイベント用ローカルサービスは準備済みです。以下のコマンドはすべて Workspace で実行します。
cd はディレクトリを選びます。Go は /usr/local/go/bin にあり、export はこのディレクトリを現在のシェルの既存の $PATH の先頭に追加します。有効なのはこのターミナルセッションだけです。新しいシェルを開いたら、同じ 2 行を再実行してください。グローバルなシェル設定ファイルの変更は不要です。
cd /home/labex/project/site-checker
export PATH=/usr/local/go/bin:$PATH
cat はファイルを編集せずに読み取ります。正常な 2 件の割り当てと Go のデータ構造を確認します。
cat normal.json types.go
各対象には、この割り当てを識別する id、リクエスト先の url、応答本文に期待する文字列 want があります。結果には id と url を残します。kind は業務上の結果、status は受信した HTTP ステータスです。応答ヘッダーを受信していなければ 0 になります。割り当てと URL は別物です。同じ入口でも、2 人が別々のチェックを依頼できます。
go run . は現在のディレクトリの Go パッケージをビルドして実行します。準備されたコマンドでは、-input で JSON 一覧、-workers で同時チェック数の上限、-timeout で各対象の HTTP 時間予算を指定します。200ms は 200 ミリ秒です。ここでは 1 つのワーカーが応答の速い 2 つの入口を順番に調べます。シェルの > は標準出力をファイルに送り、既存ファイルがあれば置き換えます。エラーは引き続きターミナルに表示されます。
go run . -input normal.json -workers 1 -timeout 200ms > normal-report.json
実際に保存された結果を読みます。
cat normal-report.json
両方の割り当てに kind: "ok" と status: 200 があり、元の ID と URL が残るはずです。200 はサーバーがこの GET を受け付けたことを示しますが、期待するページ内容の存在までは証明しません。最初の実装は簡単なケースしか理解していません。次に、ほかの入口にも意味のある結果を返すようにします。
go: command not found が出たら、このセッションの export を再実行します。ファイルが見つからなければ cd を再実行し、名前の綴りを確認します。後で Go がコンパイルエラーを報告した場合は、そのファイル名と行番号を使い、該当する小さな範囲を見つけて修正します。次のステップでは、ファイル全体を置き換えずに、編集・保存して変更を確認する方法を学びます。main.go、types.go、すべての入力 JSON、準備されたローカルサービスは変更しないでください。
応答とリクエストの失敗を区別する
このステップでは、Workspace で小さな Check 関数を、一つの判断ずつ編集します。一つの対象の意味に集中できるよう、run.go は逐次処理のままにします。まず初期実装を読みます。
cat check.go
結果は、この割り当ての ID と URL を持つ状態から始まります。client.Get は応答とエラーを返します。既存のエラー分岐は接続失敗を扱います。呼び出しが成功すると、この関数が応答本文を閉じる責任を持ちます。そのエラー分岐の直後にある defer response.Body.Close() は残します。defer は早期リターンも含め、関数が戻るときに実行されます。受信したステータスの代入も残します。用意された完成プログラムに置き換えるのではなく、この初期実装を拡張します。
編集し、その箇所で復旧する
編集前にソースのバックアップを作ります。cp はファイルをコピーします。末尾の .bak は重要です。別の .go ファイルにすると、このパッケージの一部としてコンパイルされ、Check が二重定義になる可能性があるからです。
cp check.go check.go.bak
ターミナルのエディターで既存のファイルを開きます。
nano check.go
nano では矢印キーで移動します。入力するとテキストが挿入され、Backspace と Delete は文字を削除します。Ctrl+K は現在の行を切り取るため、その行全体を削除する場合だけ使います。Ctrl は Control を押しながら、もう一つのキーを押す意味です。Ctrl+W を押して func Check と入力し、Enter で検索します。Ctrl+C は検索をキャンセルします。カーソル位置で編集します。保存するには Ctrl+O を押し、ファイル名がまだ check.go であることを確認して Enter を押します。Ctrl+X で終了します。さらに編集するたびに nano check.go を実行します。違う名前のファイルを開いたり、意図しない変更をしたりした場合は、Ctrl+X を押し、保存するか尋ねられたら N を押します。意図しないファイル名のプロンプトでは、Ctrl+C でプロンプトをキャンセルします。pwd で現在のディレクトリを読み、最初のステップの cd を使い、正しいファイルを開いてからやり直します。
各編集後に gofmt -w check.go でファイルをその場で整形します。波かっこの欠落などの構文エラーには、ファイル名と行番号が示されます。そのファイルを開き直し、近くの文を検索して該当部分を修正します。保存後に cat check.go を使い、ディスクに実際にある内容を確認します。すでに書いた動作する部分は残します。通常のソースのミスで、VM のリセットや関数全体の上書きは必要ありません。
サーバーがこの入口を受け付けたか判断する
403 応答は HTTP のやり取りが成立したことを示すので、Get はそれに対して自動的に Go エラーを返すわけではありません。このイベントは正確にステータス 200 を求め、それ以外の受信ステータスは http_error とします。これはイベントの業務規則であり、ほかの 2xx ステータスがすべて一般的に HTTP の失敗だという意味ではありません。
Check で response.StatusCode を保存した後、標準ライブラリの 200 を表す定数 http.StatusOK と比較する条件を書きます。異なるステータスなら、結果の kind を "http_error" にし、直ちにその結果を返します。今は既存の最後の "ok" 結果を残します。ここで戻ると、既存の defer によって本文が閉じられるので、不要なエラー本文を読むのを待ちません。
保存して nano を終了し、整形します。
gofmt -w check.go
実行前に、正常な割り当てとスタッフの割り当ての結果を予想します。すべての割り当てを、編集せずに確認できます。
cat targets.json
go run . -input targets.json -workers 1 -timeout 200ms
この段階では、スタッフの割り当ては 403 の http_error となり、受信した 200 ページは、内容が違ってもまだ ok のはずです。遅いチケットのチェックは、まだ connection_error と呼ばれます。タイムアウトは後で区別します。これらの意図的に残した誤りが、次の編集で何を変える必要があるかを示します。二つの受付 ID も含め、各入力 ID に引き続き結果が必要です。
受信したページの内容を確認する
200 応答では、io.ReadAll(response.Body) が本文のバイト列とエラーを返します。net/http と同じインポートグループに io と strings を追加します。import ( の後、引用符で囲んだパッケージ名を一行に一つずつ書き、) で閉じます。インポートは、そのファイルで使うパッケージを示します。未使用のインポートはコンパイルエラーなので、呼び出しを実装するときに追加します。
正常に受信した本文を分類する前に、body と err に読み取ります。読み取りに失敗したら、今は "connection_error" の結果を返します。すでに受信したステータスは結果に残します。読み取り成功後は、まず result.Kind = "ok" を設定するか、その代入を残します。その代入の後、最後の return の前に、期待するマーカーを調べます。string(body) はバイト列をテキストに変換し、strings.Contains(string(body), target.Want) はそのテキストにマーカーが含まれるかを返します。含まれなければ kind を "content_mismatch" に変えます。この条件の後で再び "ok" を代入してはいけません。不一致を上書きしてしまうからです。Go の ! はブール値を否定します。正常に読めた本文に対する最後の return は一つに保ちます。
保存して終了し、整形して、同じ実際の割り当てを繰り返します。
gofmt -w check.go
go run . -input targets.json -workers 1 -timeout 200ms
今度は誤った地図が 200 の content_mismatch となり、両方の受付は 200 の ok、スタッフは 403 の http_error のままになるはずです。ステータスを受信しただけでは、マーカーが一致した証明にはなりません。チケットのタイムアウトには、まだ独自の分類が必要です。
有限の待機時間切れと別の失敗を区別する
HTTP クライアントの時間予算には、応答本文の読み取りも含まれます。ヘッダー前のタイムアウトではステータスはゼロのままですが、200 ヘッダー後のタイムアウトでは 200 を保持しなければなりません。インポートに net を追加します。net.Error には Timeout() メソッドがあります。次の短い Go のパターンは、メソッドを呼ぶ前に、エラーがそのインターフェースに対応するかを確認します。
if networkError, ok := err.(net.Error); ok && networkError.Timeout() {
// The operation exceeded its time budget.
}
型アサーションは変換された値とブール値を返します。&& は左側が true の場合だけ右側を評価します。リクエストのエラー分岐と本文読み取りのエラー分岐の両方で、既定値は "connection_error" とし、この安全な検査が成功したら "timeout" に変えます。各分岐の return と元の作業の識別情報は残します。保存して終了し、整形します。
gofmt -w check.go
既存のサービスには、二つの有用な管理下のケースもあります。/garden/bodylate/check は、有限の 600ms の本文遅延より先に 200 ヘッダーを送り、/garden/deniedbody/check は、同じ不要な本文遅延より先に 403 ヘッダーを送ります。準備済みの JSON 一覧をすべて残したまま、これらを調べる補助入力を作ります。
nano body-probes.json
次の JSON 配列を入力します。JSON は、キーと文字列に二重引用符、フィールドとオブジェクトの間にコンマ、一覧の周囲に角かっこを使います。最後のフィールドやオブジェクトの後にコンマは置きません。
[
{"id": "body-200", "url": "http://127.0.0.1:8080/garden/bodylate/check", "want": "GARDEN OPEN"},
{"id": "body-403", "url": "http://127.0.0.1:8080/garden/deniedbody/check", "want": "GARDEN OPEN"}
]
Ctrl+O で保存し、body-probes.json と Enter で確定して、Ctrl+X で終了します。使う前に実際のファイルを読みます。
cat body-probes.json
二つの結果を予想し、200ms の予算でリクエストします。
go run . -input body-probes.json -workers 1 -timeout 200ms
body-200 はステータス 200 の timeout、body-403 はステータス 403 の http_error が期待されます。拒否は本文を読む前に判明するため、本文の遅延によってタイムアウトに変えてはいけません。クライアントのタイムアウトは待機を止めますが、サーバーは後で有限の処理を終える場合があります。総経過時間から正しさを推測しないでください。
最後に、元の完全な割り当て一覧を再実行します。
go run . -input targets.json -workers 1 -timeout 200ms
行位置ではなく ID で照合します。期待するのは二つの ok、403 の http_error、200 の content_mismatch、ゼロの timeout、ゼロの connection_error です。業務上の失敗は通常のレポート値なので、コマンドは引き続き正常に終了します。コンパイルエラーや JSON 構文エラーは、コマンドが仕事を実行できなかったという意味です。そのファイル名と位置を使ってローカルの編集を修正し、そのコマンドを繰り返します。コースのチェックは、独立した入力で元のソースと本文クローズの動作を試します。特定のエディターや補助入力のファイル名を要求するものではありません。
完全なレポートを保存して読む
このステップでは、すべての割り当てを保持しながら、役立つレポートを保存します。準備された逐次ループを読みます。
cat run.go
http.Client{Timeout: budget} は各リクエストに有限の時間予算を設定します。ループは対象ごとに Check を 1 回呼び、エラーも含めてすべての結果を追加します。最初の失敗で戻ったり、URL をキーとする map に結果を集めたりしてはいけません。初期化済みの空スライスは、割り当てが 0 件のときに空の JSON 配列も生成します。
完全なレポートを保存します。意図的に置き換えるのはレポートだけで、入力の割り当てはそのまま残します。
go run . -input targets.json -workers 1 -timeout 200ms > report.json
自分が実際に生成したファイルを読みます。
cat report.json
ID を使い、2 件の受付チェックが同じ URL を指していることを追跡します。それぞれが正確に 1 回ずつ現れる必要があります。403 の結果は、サーバーが応答してリクエストを拒否したことを担当者に伝えます。接続障害ではありません。地図のマーカーが違う場合は、200 でも内容を調べる必要があります。タイムアウトは、そのチェックが時間予算を超えたことを示すのであって、サービスが永久に応答しないという意味ではありません。
最後に、準備された空の割り当て一覧を試します。標準出力を直接表示するため、有用なレポートは置き換えません。
go run . -input empty.json -workers 1 -timeout 200ms
[] が表示され、正常に終了するはずです。report.json、更新した check.go、すべての元の入力を残してください。コースの検証はレポートを読むだけでなく、独立したローカル検証サービスに対して元のソースを実行します。コピーした行や静的な結果で、実際のリクエストを代用することはできません。
まとめ
割り当てられた各ローカル入口に正確な結果を作り、HTTP の拒否と通信の失敗を区別し、内容の不一致を識別しました。また、HTTP に有限の時間予算を適用し、受信した本文をすべて閉じました。完全な JSON レポートを保存して読み、同じ URL でも異なる作業 ID として保持しました。次の単元では、これらの判断を維持したまま、少数のワーカーが作業を分担する方法を変えます。



