🎯 最終演習

Goでアクセスログを集計してみよう(総合演習)

ここまでの Go レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。構造体・スライス・マップ・for range・関数・switch——すでに練習したものだけで組み立てます。

作るのはアクセスログの集計です。平均応答時間を出し、ステータスコードごとに数え、いちばん遅かったリクエストを見つけて、レポートにまとめます。

これはGoがいちばん得意とする仕事です。Goはサーバーやコマンドラインの道具を書くために作られた言語で、ログを読んで数えて出すのは、その中心にある用途です。

このページではコードを実行していません。Goはコンパイルが必要な言語で、ブラウザの中では走らせられないためです。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。

表示している実行結果は本物です。このページを作るときに、実際のGoでコンパイルして走らせた出力をそのまま載せています。

完成イメージ

最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。

===== アクセスログ集計 =====
リクエスト数: 8件
平均応答時間: 287.0ms
遅いリクエスト(300ms以上): 2件
--- ステータス別 ---
  200 成功                 5件
  401 クライアントエラー          1件
  404 クライアントエラー          1件
  500 サーバーエラー            1件
最も遅かった: /api/items (1420ms, status 500)

STEP 1ログ1行を構造体で表す

🎯 このステップのゴール: 型を定義し、それを並べたスライスを作る

まずデータの形を決めます。Goでは先に型を宣言するのが出発点です。

ログ1行が持つのは「パス・ステータスコード・応答時間」の3つ。type Entry struct { ... }と書いて、この3つを持つ新しい型を作ります。

フィールド名を大文字で始めているのに注目してください。Goでは大文字で始まる名前だけが、他のパッケージから見えるという決まりがあります。小文字にすると同じパッケージの中でしか使えません。アクセス修飾子の代わりを、名前の1文字目が果たしているのです。

そして[]Entry{...}スライス——伸び縮みする配列——を作ります。{Path: "/", Status: 200, MsTime: 42}のようにフィールド名を書いて初期化するのが安全です。名前を省いて順番だけで書くこともできますが、フィールドの順番を変えた瞬間に、静かに壊れます

len(logs)で件数を数えます。Goではlogs.lengthではなくlen(logs)——長さは組み込み関数で取ります。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
リクエスト数: 8件
1件目: / (status 200)

STEP 2平均応答時間を出す

🎯 このステップのゴール: for rangeで合計し、割り算のために型を変換する

応答時間を全部足して、件数で割ります。使うのはfor rangeです。

for _, e := range logs——この_(アンダースコア)が Go らしいところです。range添字と値の2つを返しますが、添字は要らない。Goでは使わない変数を宣言するとコンパイルエラーになるので、_で「捨てる」と明示します。

この厳しさはGoの設計思想です。使われていない変数はたいてい書き間違いか消し忘れなので、動かす前に止めてしまう。慣れるまで煩わしいのですが、後から効いてきます

次が割り算です。total / len(logs)と書くと整数の割り算になり、小数点以下が切り捨てられますfloat64(total) / float64(len(logs))両方を明示的に変換します。

Goは自動で型を変換しません。intfloat64を混ぜて計算しようとするだけでコンパイルエラーです。これも厳しく見えますが、「気づかないうちに精度が落ちていた」という事故が起きないという利点があります。

0件のときに割るとNaNという値になります。だからaverageの中で件数が0なら0を返すと先に書いておきます。割る前に0を確かめる——これは書く習慣にしてください。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
合計: 2296ms
平均応答時間: 287.0ms

STEP 3ステータスコードごとに数える

🎯 このステップのゴール: マップで集計し、switchで分類名を付ける

ステータスコードごとの件数を数えます。ここでマップ——キーと値の対応表——を使います。

counts := map[int]int{}は「整数をキーに、整数を値に持つマップ」という意味です。そしてcounts[e.Status]++と書くだけで数えられます。

ここがGoのマップの便利なところです。まだ無いキーを読むと、エラーではなく「その型のゼロ値」が返りますintのゼロ値は0なので、初回でも0 + 1になって正しく1になる。「キーが無ければ0を入れてから足す」という前処理が要りません。

ただしこの親切さが罠になる場面もあります。「キーが無い」のか「値が0だった」のかを区別したいときは、v, ok := counts[key]2つ受け取ってokで判定します。

分類名を付けるのがstatusLabelです。条件を書くswitch——switch { case 条件: }という形——を使うと、ifを並べるより読みやすくなります。

Goのswitchにはbreakが要りません。C言語やJavaのように、書き忘れて次のcaseに流れ込む事故が起きない設計です。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
200 成功: 5件
404 クライアントエラー: 1件
500 サーバーエラー: 1件

STEP 4マップを決まった順番で表示する

🎯 このステップのゴール: キーを取り出して並べ替えてから回す

ステップ3で作ったマップを、そのままfor rangeで回して表示すると——実行するたびに順番が変わります

これはバグではなく、Goの仕様です。マップの反復順は意図的にばらばらにされています。「順番があると思い込んだコード」を書かせないために、わざとそうしてあるのです。

他の多くの言語と違うところなので、必ず一度は踏みます。手元では正しく見えたのに、本番で並びが変わって「表示がおかしい」と言われる——よくある話です。

直し方は決まっています。キーだけを取り出して、並べ替えてから回す

codes := make([]int, 0, len(counts))で入れ物を作ります。3つ目の引数は容量——「これだけ入る見込み」を先に伝えておくと、appendのたびに配列を作り直さずに済みます。件数が分かっているなら書いておくのが作法です。

for code := range counts——値を受け取らず1つだけ書くと、キーが取れます。あとはsort.Ints(codes)で並べ替えて、その順に表示します。

%-18sで分類名の幅を揃えます。日本語は文字の幅が違うので完全には揃いませんが、大きくは整います。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
  200: 5件
  401: 1件
  404: 1件
  500: 1件

STEP 5いちばん遅かったリクエストを探す

🎯 このステップのゴール: 2つの値を返して「見つかったか」も一緒に伝える

いちばん時間のかかったリクエストを探します。やり方は1件目を暫定の1位にして、順に比べて入れ替える——集計の定番です。

問題はログが0件だったときです。返すものがありません。他の言語ならnullを返すところですが、Goにはnullを返すという発想がありません

かわりに2つの値を返しますfunc slowest(logs []Entry) (Entry, bool)——結果と、「見つかったかどうか」です。

これはGoのいたるところに現れる形です。マップのv, ok := m[key]も、ファイルを開くf, err := os.Open(...)も、同じ考え方です。「失敗するかもしれない」を戻り値で正直に表す——例外を投げて呼び出し元に押し付けない、というのがGoの立場です。

受け取る側はif worst, ok := slowest(logs); ok {と書きます。ifの中で変数を作って、その場で判定する書き方で、worstokifの中でしか使えません。使う範囲が狭いほど、間違いにくくなります。

0件のときに返しているEntry{}ゼロ値の構造体です。フィールドが全部ゼロ値(空文字と0)で埋まったもので、「中身は見ないでほしい」という印として使います。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
最も遅かった: /api/items (1420ms, status 500)
ログが0件のときは、見つからないと返ります

STEP 6ひとつの関数にまとめて仕上げる

🎯 このステップのゴール: 全部を report にまとめ、しきい値を定数にする

最後の仕上げです。ここまでの部品をreportという1つの関数にまとめます

あわせて「遅い」の境目を定数にしますconst SlowMs = 300——300という数字をコードのあちこちに直接書くと、何を意味するのか後から読んで分からなくなります

しかもこの値は必ず変わります。「100msにしよう」となったとき、定数なら1か所です。Goの定数は型を書かなくても、使う場所に合わせて解釈されます——e.MsTime >= SlowMsでも%dに渡しても、そのまま通ります。

並べる順番にも意味を持たせます。まず全体の数字、次にステータス別の内訳、最後に個別のいちばん遅かった1件——大きいものから細かいものへという流れです。読む人はまず全体を掴みたいからです。

完成です。logsに行を足したり、MsTimeの値を変えたりしてみてください。平均も件数も、いちばん遅い1件も、全部連動して変わります

SlowMsを100に変えると、「遅いリクエスト」の件数だけが増えます。1か所直すだけで意味が変わる——定数に切り出しておいた効果が、ここで確かめられます。

次に進むなら、このログをファイルから読むところです。行をstrings.Splitで分けてEntryに詰め直せば、このreportはそのまま使えます。集計の部分を関数に切り出しておいたから、入り口だけ差し替えられるのです。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
===== アクセスログ集計 =====
リクエスト数: 8件
平均応答時間: 287.0ms
遅いリクエスト(300ms以上): 2件
--- ステータス別 ---
  200 成功                 5件
  401 クライアントエラー          1件
  404 クライアントエラー          1件
  500 サーバーエラー            1件
最も遅かった: /api/items (1420ms, status 500)

よくある質問

このページではコードを実行しないのですか?

していません。Goはコンパイルが必要な言語で、ブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。

ただし表示している実行結果は本物です。このページを作るときに実際のGoでコンパイルして走らせた出力を、そのまま載せています。自分のパソコンにGoを入れれば、まったく同じ結果が出ます。

マップの表示順が実行するたびに変わります

Goの仕様です。バグではありません。マップの反復順は意図的にばらばらにされています。「順番があると思い込んだコード」を書かせないためです。

決まった順で表示したいなら、ステップ4のようにキーだけを取り出し、並べ替えてから回します。この一手間を省くと、手元では正しく見えたのに本番で並びが変わる、という見つけにくい不具合になります。

declared and not used というエラーが出ます

使っていない変数があります。Goでは変数を宣言したのに一度も使わないと、コンパイルが通りません。

for _, e := range logs_は、「受け取るけれど使わない」と明示するための書き方です。添字が要らないときは、変数名の代わりに_を書きます。

import も同じで、使っていないパッケージを import したままだと落ちます。厳しく感じますが、消し忘れが積もらない利点があります。

エラーはなぜ例外ではなく戻り値なのですか

「失敗するかもしれない」ことを、呼び出す側から隠さないためです。

例外は、書かなくても勝手に上へ飛んでいきます。便利ですが、どの行が失敗しうるのかがコードから読み取れません

Goは戻り値で返すので、if err != nilを書くまで先に進めません。冗長に見えますが、失敗の扱いを書き忘れることがなくなります。ステップ5の(Entry, bool)も、同じ考え方の小さな例です。

🚩 次はこのカテゴリへ

Goで書いた道具は、1つの実行ファイルにまとまるのが大きな強みです。それをそのままコンテナに入れれば、どこでも同じように動きます。Dockerカテゴリの最終演習では、イメージの取得から起動・片付けまでを通しで練習します。自分で書いたプログラムを世に出すとき、次に通る道です。

Dockerのレッスンへ進む
広告スペース(バナー)
広告スペース(記事内)