[インデックス 10587] ファイルの概要
このコミットは、Go言語のダッシュボードアプリケーションにおけるAPIレスポンスのフォーマットを統一し、コミット情報の取得(GETモード)を実装するものです。主にmisc/dashboard/app/build/build.goとmisc/dashboard/app/build/test.goの2つのファイルが変更されています。
misc/dashboard/app/build/build.go: ダッシュボードのビルド関連ロジックとHTTPハンドラが含まれる主要なファイルです。このコミットでは、既存のハンドラ関数のシグネチャが変更され、新しいGETモードがcommitHandlerに追加されました。また、レスポンスの統一化とエラーハンドリングの改善が行われています。misc/dashboard/app/build/test.go:build.goで定義されたハンドラ関数のテストコードが含まれています。APIレスポンスフォーマットの変更に伴い、テストロジックも更新されています。
コミット
commit 6c165d7ac461d86c6ce5c69c09ae170eaf1608dc
Author: Andrew Gerrand <adg@golang.org>
Date: Fri Dec 2 16:05:12 2011 +1100
dashboard: make response format consistent, implement commit GET mode
R=golang-dev, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5437113
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6c165d7ac461d86c6ce5c69c09ae170eaf1608dc
元コミット内容
dashboard: make response format consistent, implement commit GET mode
R=golang-dev, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5437113
変更の背景
このコミットの主な背景は、Go言語のビルドダッシュボード(おそらくGoプロジェクト自体のCI/CDステータスを表示するシステム)のAPI設計の改善にあります。
- レスポンスフォーマットの一貫性: 以前のハンドラ関数は、直接
http.ResponseWriterにデータを書き込んでいました。これにより、各ハンドラが独自のレスポンス形式を持つ可能性があり、クライアント側でのAPI利用が複雑になる問題がありました。この変更により、すべてのAPIレスポンスが統一されたJSON形式(データとエラーを明示的に含む構造)で返されるようになり、クライアント側でのパースとエラーハンドリングが簡素化されます。 - コミット情報のGETモード実装: これまで
commitHandlerは新しいコミット情報を記録する(POST)機能のみを持っていました。しかし、ダッシュボードの機能として、特定のコミット情報を取得するニーズが発生したと考えられます。この変更により、コミットハッシュやパッケージパスを指定して、既存のコミット情報を取得できるようになります。これは、ビルド履歴の表示や特定のビルド結果の参照といった機能に不可欠です。 - Go App Engineの利用: 当時のGo言語プロジェクトでは、Google App Engine (GAE) を利用してダッシュボードが構築されていました。GAEのDatastoreとの連携や、GAEのコンテキストに合わせたエラーロギングの改善も、この変更の動機の一部です。
これらの変更は、ダッシュボードのAPIをより堅牢で使いやすくし、将来的な機能拡張に対応するための基盤を強化することを目的としています。
前提知識の解説
このコミットを理解するためには、以下の技術的知識が役立ちます。
-
Go言語のHTTPパッケージ (
net/http):http.HandlerFunc: HTTPリクエストを処理するための関数型です。通常、func(w http.ResponseWriter, r *http.Request)というシグネチャを持ちます。http.ResponseWriterはHTTPレスポンスを書き込むためのインターフェースで、*http.Requestは受信したHTTPリクエストの詳細を含みます。http.Request.Method: HTTPリクエストのメソッド(GET, POSTなど)を文字列で取得します。http.Request.FormValue(): URLクエリパラメータやフォームデータから指定されたキーの値を取得します。http.DefaultServeMux: Goの標準HTTPルーターで、リクエストパスに基づいてハンドラをディスパッチします。httptest.NewRecorder(): テスト時にHTTPレスポンスを記録するためのhttp.ResponseWriterの実装です。
-
Go言語のJSONパッケージ (
encoding/json):json.NewDecoder(r.Body).Decode(v): HTTPリクエストボディからJSONデータを読み込み、Goの構造体にデコードします。json.NewEncoder(w).Encode(v): Goの構造体をJSONデータにエンコードし、http.ResponseWriterに書き込みます。
-
Google App Engine (GAE):
- Datastore: Google Cloud Platformが提供するNoSQLデータベースサービスです。GAEアプリケーションの永続データストアとして利用されます。
appengine.NewContext(r): App EngineのAPIを呼び出すために必要なコンテキストを作成します。datastore.Get(c, key, dst): Datastoreからエンティティ(データレコード)を取得します。datastore.Put(c, key, src): Datastoreにエンティティを保存または更新します。datastore.RunInTransaction(c, tx, opts): トランザクション内で一連のDatastore操作を実行します。これにより、複数の操作がアトミックに(すべて成功するか、すべて失敗するかのいずれか)実行されることが保証されます。datastore.NewQuery(kind): Datastoreからエンティティをクエリするためのオブジェクトを作成します。datastore.Done: クエリ結果のイテレーションが終了したことを示すエラーです。
- エラーロギング:
appengine.Contextには、GAEのログサービスにメッセージを書き込むためのメソッド(Errorf,Criticalfなど)が含まれています。
- Datastore: Google Cloud Platformが提供するNoSQLデータベースサービスです。GAEアプリケーションの永続データストアとして利用されます。
-
Go言語のエラーハンドリング (
os.Errorとerrorインターフェース):- このコミットが作成された2011年当時、Go言語では
os.Errorという型がエラーを表すために広く使われていました。これは後に組み込みのerrorインターフェースに統合されますが、このコードベースではまだos.Errorが使われています。os.ErrorはError() stringメソッドを持つインターフェースです。
- このコミットが作成された2011年当時、Go言語では
-
ビルドダッシュボードの概念:
- 継続的インテグレーション(CI)/継続的デリバリー(CD)システムの一部として、ソフトウェアプロジェクトのビルド、テスト、デプロイのステータスをリアルタイムで表示するWebアプリケーションです。ビルドの成功/失敗、テスト結果、コミット情報などを一元的に管理・可視化します。
技術的詳細
このコミットの技術的な変更点は多岐にわたりますが、特に重要なのは以下の点です。
-
ハンドラ関数のシグネチャ変更と
dashHandler型、dashResponse構造体の導入:- 従来のHTTPハンドラは
func(w http.ResponseWriter, r *http.Request)というシグネチャで、直接wにレスポンスを書き込んでいました。 - このコミットでは、
dashHandler func(*http.Request) (interface{}, os.Error)という新しい関数型が導入されました。これにより、ビジネスロジックを担うハンドラ関数は、HTTPレスポンスの書き込みから解放され、処理結果のデータ(interface{})とエラー(os.Error)を返すことに専念できるようになります。 dashResponse構造体は、Response interface{}とError os.Errorという2つのフィールドを持ちます。これは、すべてのAPIレスポンスがこの統一されたJSON構造で返されることを意味します。クライアントは常にこの構造をパースし、Errorフィールドがnilでなければエラーが発生したと判断できます。
- 従来のHTTPハンドラは
-
AuthHandlerの役割の拡張:AuthHandlerは元々、認証(キーとビルダーのクエリパラメータの検証)を行うためのミドルウェアでした。- 変更後、
AuthHandlerはdashHandler型の関数を引数に取り、http.HandlerFuncを返します。この内部で、ラップされたdashHandlerが呼び出され、その戻り値(データとエラー)がdashResponse構造体に格納され、JSONとしてhttp.ResponseWriterに書き込まれます。 - これにより、認証とレスポンスのJSONエンコードという共通処理が
AuthHandlerに集約され、各ビジネスロジックハンドラはよりシンプルになりました。エラーが発生した場合も、dashResponseのErrorフィールドにエラー情報が設定され、クライアントに返されます。また、App Engineのコンテキストを使ったエラーロギング(c.Errorf,c.Criticalf)もここで行われます。
-
commitHandlerへのGETモードの実装:commitHandlerは、HTTPメソッドがGETの場合に、packagePathとhashというクエリパラメータを受け取り、Datastoreから対応するCommitエンティティを取得する機能が追加されました。これにより、特定のコミット情報をAPI経由で取得できるようになります。POSTリクエストの場合は、従来通りリクエストボディからJSONエンコードされたCommitデータをデコードし、Datastoreに保存します。
-
エラーハンドリングの統一:
- 各ハンドラ関数内で直接
logErr(w, r, err)を呼び出してエラーをログに記録し、レスポンスを終了する代わりに、nil, errを返すようになりました。これにより、エラー処理はAuthHandlerに一元化され、dashResponseを通じてクライアントにエラー情報が伝達されます。
- 各ハンドラ関数内で直接
-
テストコードの適応:
misc/dashboard/app/build/test.go内のテストケースは、新しいAPIレスポンスフォーマットに対応するために修正されました。特に、httptest.NewRecorder()で記録されたHTTPレスポンスボディを、直接文字列として比較するのではなく、dashResponse構造体にJSONデコードしてから、そのResponseフィールドやErrorフィールドを検証するように変更されています。これにより、実際のAPIの挙動をより正確にテストできるようになりました。
これらの変更は、Go言語のWebアプリケーション開発における一般的なパターン、すなわち「ミドルウェアによる共通処理の集約」と「APIレスポンスの一貫性確保」を早期に実践している例と言えます。
コアとなるコードの変更箇所
misc/dashboard/app/build/build.go
// commitHandler records a new commit. It reads a JSON-encoded Commit value
// from the request body and creates a new Commit entity.
// commitHandler also updates the "tip" Tag for each new commit at tip.
//
// This handler is used by a gobuilder process in -commit mode.
-func commitHandler(w http.ResponseWriter, r *http.Request) {
+func commitHandler(r *http.Request) (interface{}, os.Error) {
+ c := appengine.NewContext(r)
com := new(Commit)
+
+ // TODO(adg): support unauthenticated GET requests to this handler
+ if r.Method == "GET" {
+ com.PackagePath = r.FormValue("packagePath")
+ com.Hash = r.FormValue("hash")
+ if err := datastore.Get(c, com.Key(c), com); err != nil {
+ return nil, err
+ }
+ return com, nil
+ }
+
+ // POST request
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(com); err != nil {
- logErr(w, r, err)
- return
+ return nil, err
}
if err := com.Valid(); err != nil {
- logErr(w, r, err)
- return
+ return nil, err
}
tx := func(c appengine.Context) os.Error {
return addCommit(c, com)
}
- c := appengine.NewContext(r)
- if err := datastore.RunInTransaction(c, tx, nil); err != nil {
- logErr(w, r, err)
- }
+ return nil, datastore.RunInTransaction(c, tx, nil)
}
// ... (tagHandler, todoHandler, packagesHandler, resultHandlerも同様のシグネチャ変更とエラーハンドリングの変更)
type dashHandler func(*http.Request) (interface{}, os.Error)
type dashResponse struct {
Response interface{}
Error os.Error
}
// AuthHandler wraps a http.HandlerFunc with a handler that validates the
// supplied key and builder query parameters.
-func AuthHandler(h http.HandlerFunc) http.HandlerFunc {
+func AuthHandler(h dashHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Put the URL Query values into r.Form to avoid parsing the
// request body when calling r.FormValue.
@@ -435,7 +442,17 @@ func AuthHandler(h http.HandlerFunc) http.HandlerFunc {
}
}
- h(w, r) // Call the original HandlerFunc.
+ // Call the original HandlerFunc and return the response.
+ c := appengine.NewContext(r)
+ resp, err := h(r)
+ if err != nil {
+ c.Errorf("%v", err)
+ }
+ w.Header().Set("Content-Type", "application/json")
+ err = json.NewEncoder(w).Encode(dashResponse{resp, err})
+ if err != nil {
+ c.Criticalf("%v", err)
+ }
}
}
misc/dashboard/app/build/test.go
// ...
func testHandler(w http.ResponseWriter, r *http.Request) {
// ...
for i, t := range testRequests {
// ...
// 以前は直接rec.Body.String()を比較していたが、dashResponseをデコードするように変更
resp := new(dashResponse)
if strings.HasPrefix(t.path, "/log/") {
resp.Response = rec.Body.String()
} else {
err := json.NewDecoder(rec.Body).Decode(resp)
if err != nil {
errorf("decoding response: %v", err)
return
}
}
if e, ok := t.res.(string); ok {
g, ok := resp.Response.(string) // resp.Responseから値を取得
if !ok {
errorf("Response not string: %T", resp.Response)
return
}
if g != e {
errorf("response mismatch: got %q want %q", g, e)
return
}
}
if t.res == nil && resp.Response != nil { // 期待値がnilの場合のチェック
errorf("response mismatch: got %q expected <nil>",
resp.Response)
return
}
}
fmt.Fprint(w, "PASS")
}
コアとなるコードの解説
misc/dashboard/app/build/build.go
commitHandler関数のシグネチャ変更:-func commitHandler(w http.ResponseWriter, r *http.Request): 変更前のシグネチャ。http.ResponseWriterを直接受け取り、レスポンスを書き込んでいました。+func commitHandler(r *http.Request) (interface{}, os.Error): 変更後のシグネチャ。http.Requestのみを受け取り、処理結果のデータ(interface{})とエラー(os.Error)を返します。これにより、この関数は純粋なビジネスロジックに集中できるようになります。
appengine.NewContext(r): App EngineのAPIを呼び出すためのコンテキストをリクエストから取得します。if r.Method == "GET"ブロック:- HTTPメソッドが
GETの場合の新しいロジックです。 com.PackagePath = r.FormValue("packagePath")とcom.Hash = r.FormValue("hash"): URLクエリパラメータからpackagePathとhashの値を取得し、Commit構造体に設定します。if err := datastore.Get(c, com.Key(c), com); err != nil: 設定されたpackagePathとhashに基づいて、Datastoreから対応するCommitエンティティを取得します。エラーが発生した場合は、nil, errを返して呼び出し元にエラーを伝播させます。return com, nil: 正常にコミット情報が取得できた場合、そのCommitオブジェクトとnilエラーを返します。
- HTTPメソッドが
// POST requestブロック:- HTTPメソッドが
POSTの場合の既存ロジックです。 defer r.Body.Close(): リクエストボディのクローズを遅延実行します。if err := json.NewDecoder(r.Body).Decode(com); err != nil: リクエストボディからJSONデータを読み込み、Commit構造体にデコードします。デコードエラーが発生した場合はnil, errを返します。if err := com.Valid(); err != nil: デコードされたCommitデータのバリデーションを行います。バリデーションエラーが発生した場合はnil, errを返します。tx := func(c appengine.Context) os.Error { return addCommit(c, com) }:addCommit関数をトランザクション内で実行するためのクロージャを定義します。return nil, datastore.RunInTransaction(c, tx, nil):addCommitをトランザクション内で実行し、その結果のエラーを返します。成功した場合はnil, nilが返されます。以前はlogErrを呼び出していましたが、ここではエラーを返すのみです。
- HTTPメソッドが
type dashHandler func(*http.Request) (interface{}, os.Error):- 新しい関数型
dashHandlerの定義です。これは、HTTPリクエストを受け取り、処理結果のデータとエラーを返すハンドラ関数の新しい標準シグネチャとなります。
- 新しい関数型
type dashResponse struct { Response interface{}; Error os.Error }:- 統一されたAPIレスポンスの構造を定義する新しい構造体です。
Responseフィールドには実際のデータが、Errorフィールドにはエラー情報が格納されます。
- 統一されたAPIレスポンスの構造を定義する新しい構造体です。
AuthHandler関数のシグネチャ変更と内部ロジックの変更:-func AuthHandler(h http.HandlerFunc) http.HandlerFunc: 変更前のシグネチャ。http.HandlerFuncをラップしていました。+func AuthHandler(h dashHandler) http.HandlerFunc: 変更後のシグネチャ。新しいdashHandler型をラップするように変更されました。これにより、AuthHandlerはビジネスロジックハンドラが返すinterface{}, os.Errorを受け取って処理できるようになります。resp, err := h(r): ラップされたdashHandlerを呼び出し、その戻り値(データとエラー)を取得します。if err != nil { c.Errorf("%v", err) }:dashHandlerからエラーが返された場合、App Engineのコンテキストを使ってエラーをログに記録します。w.Header().Set("Content-Type", "application/json"): レスポンスのContent-Typeをapplication/jsonに設定します。err = json.NewEncoder(w).Encode(dashResponse{resp, err}):dashResponse構造体(取得したデータとエラーを含む)をJSONにエンコードし、http.ResponseWriterに書き込みます。この処理自体でエラーが発生した場合もc.Criticalfでログに記録されます。
misc/dashboard/app/build/test.go
testHandler関数の変更:resp := new(dashResponse): 新しいdashResponse構造体のインスタンスを作成します。if strings.HasPrefix(t.path, "/log/"):/log/パスの場合(ログデータはJSONではないため)は、レスポンスボディを直接resp.Responseに設定します。else { err := json.NewDecoder(rec.Body).Decode(resp) ... }: それ以外のパスの場合、httptest.NewRecorder()で記録されたレスポンスボディをdashResponse構造体にJSONデコードします。これにより、テストコードは統一されたAPIレスポンスフォーマットを正しく解釈できるようになります。if e, ok := t.res.(string); ok { g, ok := resp.Response.(string) ... }: 期待される結果が文字列の場合、resp.Responseフィールドから文字列を取得し、それを期待値と比較します。以前はrec.Body.String()を直接比較していましたが、dashResponseの内部構造を考慮した比較に変更されました。if t.res == nil && resp.Response != nil: 期待される結果がnilであるにもかかわらず、resp.Responseにデータが含まれている場合のチェックが追加されました。これは、todoHandlerが結果を返さない場合にnilを返すようになった変更に対応しています。
これらの変更により、Go言語のダッシュボードアプリケーションは、よりモジュール化され、APIの使いやすさと堅牢性が向上しました。
関連リンク
- Go言語公式ドキュメント: https://go.dev/doc/
net/httpパッケージ: https://pkg.go.dev/net/httpencoding/jsonパッケージ: https://pkg.go.dev/encoding/json- Google App Engine (Go): https://cloud.google.com/appengine/docs/standard/go/ (当時のドキュメントは現在のものとは異なる可能性があります)
- Go Datastoreクライアントライブラリ: https://pkg.go.dev/cloud.google.com/go/datastore (当時のApp Engine SDKのDatastore APIとは異なる可能性があります)
参考にした情報源リンク
- Go言語の公式リポジトリ (GitHub): https://github.com/golang/go
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/5437113 (コミットメッセージに記載されているCLリンク)
- Go言語の歴史に関する情報 (当時の
os.Errorからerrorインターフェースへの移行など) - Google App Engineの歴史的ドキュメント (当時のGo SDKの挙動を理解するため)
- 一般的なWeb API設計のベストプラクティス(統一されたレスポンスフォーマット、ミドルウェアの利用など)