uconはMiddlewareとPluginによる柔軟な拡張が可能な、Golangのウェブアプリケーションフレームワークです。
go get -u github.com/favclip/ucon
uconを始めるには、サーバーを起動するためのgoファイルが必要です。まずはmain.go
を作成し、次のようにmain関数を実装します。
package main
import (
"net/http"
"github.com/favclip/ucon/v3"
)
func main() {
ucon.Orthodox()
ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
})
ucon.ListenAndServe(":8080")
}
次にgo run
コマンドでサーバーを起動してみましょう。
go run main.go
Webブラウザでlocalhost:8080
にアクセスすると、uconによって返されたHello World!
という文字列が表示されるでしょう!
もっとサンプルが見たい方は/sample
ディレクトリの中にあるいくつかの例を見てみるといいかもしれません。
- 標準のnet/httpとの互換性
- 柔軟なルーティング設定
- Middlewareによるリクエストハンドラの拡張
- 強力なDI(依存性注入)機構
- Pluginによるサーバー機能の拡張
Orthodox()
による標準的な機能の提供- テスト支援のための便利なユーティリティ
- Google App Engineなどの様々なプラットフォームで利用可能
- Swagger(Open API Initiative)に対応する
swagger
Plugin
uconを使ったサーバーを起動するにはucon.ListenAndServe
関数を実行します。
この関数は引数がアドレスだけであるという点を除いて、http.ListenAndServe
と完全に互換性があります。
既存のサーバー上でuconを使用する場合は「既存のサーバーへの組み込み」を参照してください。
uconのルーティングはHandle
関数、またはHandleFunc
関数によって設定されます。
HandleFunc
関数はリクエストハンドラとして関数オブジェクトを登録します。
関数だけでは表現できない複雑なリクエストハンドリングを行いたい場合は、HandlerContainerインタフェースを実装したオブジェクトをHandle
関数で登録することもできます。
uconが標準のhttp
パッケージと違うのは、uconではルーティングの定義にHTTPのリクエストメソッドが必須であることです。
(これはGoogle Cloud EndpointsやSwaggerといったプラットフォームとの親和性を高める上で必要なアプローチでした。)
同じパスへのリクエストでも、リクエストメソッドが違う場合はそれぞれにリクエストハンドラが個別に必要です。
ただし、ルーティング定義のリクエストメソッドをワイルドカード(*
)に指定した場合はすべてのリクエストメソッドに対して有効なリクエストハンドラになります。
HandleFunc("GET", "/a/", ...)
は、GETリクエストの/a/b
や/a/b/c
などにはマッチしますが、 POSTリクエストの/a/b
にはマッチしませんし、GETリクエストの/a
にもマッチしません。HandleFunc("*", "/", ...)
は、すべてのリクエストにマッチします。- (1)
HandleFunc("GET", "/a", ...)
と(2)HandleFunc("GET", "/a/", ...)
の2つがある場合、 GETリクエストの/a
は(1)にマッチしますが、/a/b
は(2)にマッチします。 HandleFunc("GET", "/users/{id}", ...)
は、/users/1
や/users/foo/bar
にマッチしますが、/users
にはマッチしません。
uconにおけるMiddlewareとは、サーバーがリクエストを受け取ってからリクエストハンドラに届けられるまでの間に実行されるプリプロセッサのことです。
いくつかのMiddlewareがuconには標準で用意されており、ucon.Orthodox()
を実行すると次のMiddlewareが読み込まれます。
- ResponseMapper - リクエストハンドラの戻り値をJSONに変換する
- HTTPRWDI -
http.Request
とhttp.ResponseWriter
のDIを行う - NetContextDI -
context.Context
のDIを行う - RequestObjectMapper - リクエストに含まれるパラメータやデータをリクエストハンドラの引数にある型のオブジェクトに変換する
もちろん独自のMiddlewareを作成することもできます。ここでは例としてリクエストを受け取るたびに標準出力にログを書き込むMiddlewareを作ってみましょう。
Middlewareの実体は、func(b *ucon.Bubble) error
で表現される関数です。
main.go
に次のようなLogger
関数を定義します。
func Logger(b *ucon.Bubble) error {
fmt.Printf("Received: %s %s\n", b.R.Method, b.R.URL.String())
return b.Next()
}
あとはLogger
をMiddlewareとして登録するだけです。Middlewareを登録するにはucon.Middleware()
関数を呼び出します。
package main
import (
"fmt"
"net/http"
"github.com/favclip/ucon/v3"
)
func main() {
ucon.Orthodox()
ucon.Middleware(Logger)
ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
})
ucon.ListenAndServe(":8080")
}
func Logger(b *ucon.Bubble) error {
fmt.Printf("Received: %s %s\n", b.R.Method, b.R.URL.String())
return b.Next()
}
これでmain.go
を実行してサーバーを立ち上げると、リクエストを受け取るたびにログを出力するようになりました。
Middlewareに与えられるBubble
は、リクエストがサーバーに届いてから、適切なリクエストハンドラに到達までの間のデータの運搬を行っています。
Bubble.Next()
を呼び出すと、次のMiddlewareに処理が移り、すべてのMiddlewareが処理を終えたらucon.Handle
かucon.HandleFunc
で登録された
リクエストハンドラの処理が実行されます。
uconのDI機構はMiddlewareがBubble.Arguments
に値を格納することで解決しています。
リクエストハンドラとして登録された関数の引数はBubble.ArgumentTypes
に格納されるため、
MiddlewareでBubble.ArgumentTypes
を見て、任意の型に対して値を与えることができます。
例えば、リクエストハンドラにtime.Time
型の引数を追加する際は、次のようなMiddlewareでDIを追加することができます。
package main
import (
"fmt"
"net/http"
"reflect"
"time"
"github.com/favclip/ucon/v3"
)
func main() {
ucon.Orthodox()
ucon.Middleware(NowInJST)
ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request, now time.Time) {
w.Write([]byte(
fmt.Sprintf("Hello World! : %s", now.Format("2006/01/02 15:04:05")))
)
})
ucon.ListenAndServe(":8080")
}
func NowInJST(b *ucon.Bubble) error {
for idx, argT := range b.ArgumentTypes {
if argT == reflect.TypeOf(time.Time{}) {
b.Arguments[idx] = reflect.ValueOf(time.Now())
break
}
}
return b.Next()
}
uconにおけるPluginとは、ucon全体の機能を拡張するためのプリプロセッサです。
Middlewareのようにリクエストが来るたびに実行されるのではなく、サーバーが起動する際に一度だけ実行されます。
sampleのswagger
は、具体的なPluginの使い方を知るための手助けになるでしょう。
Pluginを実装するには、Pluginのインタフェースを実装した構造体のオブジェクトをucon.Plugin()
関数で登録します。
現在、uconでは以下のPluginインタフェースが提供されています。
- HandlersScannerPlugin - uconに登録されたリクエストハンドラの一覧を取得できる
Pluginには*ServeMux
が引数で与えられるので、PluginによってリクエストハンドラやMiddlewareを追加することも可能です。
uconではuconを使ったアプリケーションの単体テストを行うのに便利なユーティリティも提供しています。
MakeMiddlewareTestBed
はMiddlewareのテストを行うためのテストベッドを提供します。
例えば、NetContextDI
Middlewareのテストは次のように記述されています。
func TestNetContextDI(t *testing.T) {
b, _ := MakeMiddlewareTestBed(t, NetContextDI, func(c context.Context) {
if c == nil {
t.Errorf("unexpected: %v", c)
}
}, nil)
err := b.Next()
if err != nil {
t.Fatal(err)
}
}
MakeHandlerTestBed
はリクエストハンドラのテストを行うためのテストベッドを提供します。
この関数を呼び出す前にリクエストハンドラが登録されている必要があります。
routing_test.goではこの関数を使って次のようなテストが記述されています。
func TestRouterServeHTTP1(t *testing.T) {
DefaultMux = NewServeMux()
Orthodox()
HandleFunc("PUT", "/api/test/{id}", func(req *RequestOfRoutingInfoAddHandlers) (*ResponseOfRoutingInfoAddHandlers, error) {
if v := req.ID; v != 1 {
t.Errorf("unexpected: %v", v)
}
if v := req.Offset; v != 100 {
t.Errorf("unexpected: %v", v)
}
if v := req.Text; v != "Hi!" {
t.Errorf("unexpected: %v", v)
}
return &ResponseOfRoutingInfoAddHandlers{Text: req.Text + "!"}, nil
})
DefaultMux.Prepare()
resp := MakeHandlerTestBed(t, "PUT", "/api/test/1?offset=100", strings.NewReader("{\"text\":\"Hi!\"}"))
if v := resp.StatusCode; v != 200 {
t.Errorf("unexpected: %v", v)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if v := string(body); v != "{\"text\":\"Hi!!\"}" {
t.Errorf("unexpected: %v", v)
}
}
uconのServeMux
はhttp.Handler
を実装しているので、http.Handle
関数に渡すことで
既存のGolangのサーバーに簡単に組み込むことができます。
ただしHandle
関数に渡す前に、通常はucon.ListenAndServe
の内部で実行されているServeMux#Prepare
関数を明示的に実行する必要があります。
デフォルトのServeMux
の参照は、ucon.DefaultMux
で取得することができます。
func init() {
ucon.Orthodox()
...
ucon.DefaultMux.Prepare()
http.Handle("/", ucon.DefaultMux)
}
MIT