[インデックス 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設計のベストプラクティス(統一されたレスポンスフォーマット、ミドルウェアの利用など)