Golang 1.9以降推奨です(その前のバージョンは使ったことがないので)
Lambdaによるサーバーレスアーキテクチャの現状 with Golang
Lambda + ApiGatewayによるサーバーレスの構成はよくみる光景です。
APIGatewayのProxyRequest/Responseを利用して一つのfunctionにPathとかformDataとかのHTTP情報を全部投げ込んでLambda側でルーティングなども行うスタイルがおそらく作りやすい、形なのかなと思います。
その際にGolang側でどのWebフレームワークを採用するか
awsによるサンプルでは「GIN:https://github.com/gin-gonic/gin」をつかっています
node likeな軽量フレームワークを謳っています。速度は調べていないですが早いそうです。
モデルバインディングをして処理するサンプルがこんな感じです(上記URLから引用したので、古いかもです。)
// Example for binding JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
awsによるgin + apigatewayのサンプルがこれです
var initialized = false
var ginLambda *ginadapter.GinLambda
func Handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
if !initialized {
// stdout and stderr are sent to AWS CloudWatch Logs
log.Printf("Gin cold start")
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
ginLambda = ginadapter.New(r)
initialized = true
}
// If no name is provided in the HTTP request body, throw an error
return ginLambda.Proxy(req)
}
func main() {
lambda.Start(Handler)
}
node使いの方にはやりやすい気がしますね。golangだとこれがメジャーなのでしょうか。
ですが、僕はこのbindingを毎回手動で書かなければならないのがとてもいやでした。また、gin.Hみたいなテンプレートの構文を書いたりするのがとても嫌いでした。Service・ビジネスロジック層を記述する際にはこのような部分はすべて省略したいです。
また、JavaのSpringBootフレームワークではアノテーションを付与しておけば自動的にバインディングしてくれあした。
Spring Boot likeなオレオレフレームワーク with Golang
そこで、Golangも始めたばかりなので、自動でバインディングをしてくれるようなフレームワークを
Reflectionのお勉強がてら作ってみました。
やりたかった機能は2つ
- target-typed binding(正式名称は不明です)
- automatic json encoding
- (レスポンス形式を自動でjsonにする)
- lambda * api gatewayであればjsonで返すのが普通かなと思いとりあえずjsonのみ対応
できた
最終的に以下のような形になりました (実際のコードは1ファイルではなく分割しています。
type TestParam struct {
UserId string
Name string
AAA string `pgway_binding:testParam` // you can bind with the query parameters like this "/test?testParam=1" -> AAA = 1
}
type TestResponse struct {
Body string
}
func api1(testParam TestParam) interface{} {
return TestResponse{
Body: testParam.UserId + "_" + testParam.Name + "_a:" + testParam.AAA,
}
}
func PgwayHandler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
apis := api.PgwayApis{
api.PgwayApi{
Path: "/api1",
HTTPMethod: http.MethodGet,
Handler: api1,
},
}
server := api.PgwayServer{
Apis: apis,
BindingNamingStrategy: api.BindingStrategyCamelCaseToSnakeCase, // you can bind with the query parameters like this "/test?user_id=1" -> UserId = 1
}
return server.HandleAPIGateway(req), nil
}
func main() {
// lambda.Start(HandlerByAws)
lambda.Start(PgwayHandler)
}
bindingやjson convertの記述はすべて省略できたかと思います。すこし冗長な気もしますが、
個人的にはこちらの方がしっくりきます。
仕組み
- APIGateway Requestなどの外部サービスリクエストを透過的に扱うためにモデルマッピング
- serverに登録したapi一覧からURLをもとに(現在は単純に最長一致で)検索する
- apiに登録されたfuncの情報を使ってフォーム作成、関数実行、レスポンス取得をreflectionにより作動
- レスポンスをjsonマッピング
- レスポンスモデルからAPIGateway など外部サービスのレスポンスモデルへとマッピング
という流れです。
コードはhttps://github.com/paragura/pgway にあるのでreflectionの部分のみ解説します。
import (
"github.com/huandu/xstrings"
r "reflect"
"strings"
)
type PgwayBindingNamingStrategy int
const (
BindingStrategyKeep = iota
BindingStrategyCamelCaseToSnakeCase
BindingStrategySnakeCaseToCamelCase
)
func CallFunc(handler interface{}, data map[string]string, bindingNamingStrategy PgwayBindingNamingStrategy, validationFailedProcessor func([]string) interface{}) interface{} {
// 関数のValue取得
method := r.ValueOf(handler)
if method.Kind() != r.Func {
// 関数じゃなきゃだめ
panic("[pgway]definition error. Handler must be a func")
}
// 関数の型情報取得
methodType := method.Type()
// 関数実行する際の引数リスト(実際に引数として関数側で処理される値)作成
in := make([]r.Value, methodType.NumIn())
if methodType.NumIn() > 0 {
for i := 0; i < methodType.NumIn(); i++ {
// 引数の型情報をもとにインスタンス作成
p := methodType.In(i)
obj := CreateInstance(p, data, bindingNamingStrategy)
// 簡単にバリデーション
//
// validation with tag
// validation runs when pgway_v: true written on struct tags
failedFields := ValidateInstance(obj)
if len(failedFields) > 0 {
//
// failed validation.
return validationFailedProcessor(failedFields)
}
// インスタンスが正しく作られていたら引数リストに投入
in[i] = r.ValueOf(obj)
}
}
// 関数実行!
out := method.Call(in)
// 返り値は単純化のために一つのみ採用
switch len(out) {
case 0:
return nil
case 1:
return out[0].Interface()
default:
panic("invalid definition")
}
}
func CreateInstance(p r.Type, data map[string]string, bindingNamingStrategy PgwayBindingNamingStrategy) interface{} {
obj := r.New(p).Elem()
for i := 0; i < p.NumField(); i++ {
structField := p.Field(i)
objField := obj.Field(i)
var keyName string
if tagValue, ok := structField.Tag.Lookup(PgwayBindingTagName); ok {
keyName = tagValue
} else {
switch bindingNamingStrategy {
case BindingStrategyCamelCaseToSnakeCase:
keyName = xstrings.ToSnakeCase(structField.Name)
case BindingStrategySnakeCaseToCamelCase:
keyName = xstrings.ToCamelCase(structField.Name)
case BindingStrategyKeep:
keyName = structField.Name
default:
panic("unknown Binding Strategy Type")
}
//
// naming conversion type (camel, snake, keep)
}
//
// TODO: string 以外
objField.Set(r.ValueOf(data[keyName]))
}
return obj.Interface()
}
//
// validation with tags. ( validate if written pgway_v: true)
// return failed field name
func ValidateInstance(obj interface{}) []string {
value := r.ValueOf(obj)
valueType := value.Type()
var failedFieldNames []string
for i := 0; i < value.NumField(); i++ {
field := value.Field(i)
fieldType := valueType.Field(i)
if tagValue, ok := fieldType.Tag.Lookup(PgwayValidationTagName); ok && strings.TrimSpace(tagValue) == "true" {
//
// need validation
if !validateField(field) {
failedFieldNames = append(failedFieldNames, fieldType.Name)
}
}
}
return failedFieldNames
}
func validateField(field r.Value) bool {
//
// TODO: string 以外
return len(field.String()) > 0
}
サーバー内でapiからHandler (funcのことです)と、リクエストのparameter(query,postData)をmap[string]stringにしたものを引数にCallFuncを呼び出しています。
CallFuncでは
handlerの引数のStructの型を取得して、その型に対してCreateInstanceを呼び出します。
CreateInstanceでは、型情報からinstance作成、およびそのinstanceに対してリクエストdataからバインディングを行います。bindingNamingStrategyは、structのkey名とdataのkey名のマッピングをcamelCaseで行うか、SnakeCaseで行うかを設定する値です。
method := r.ValueOf(handler)
とすることで該当のオブジェクトをreflectionで扱うためのアクセスオブジェクト(関数もstructも通常のstringなども透過的に扱えます)を取得できます。
methodType := method.Type()
とすると該当のアクセスオブジェクトから型情報(reflect.Type型)を取得できます。関数であれば
methodType.In() // 引数の型情報配列
methodType.NumIn() // 引数の数
metdhoType.Out() // 返り値の....
といった情報が取得できるのでその値をもとにインスタンスを作成します。
インスタンス生成
つづくCreateInstance関数で、
func CreateInstance(p r.Type, data map[string]string, bindingNamingStrategy PgwayBindingNamingStrategy) interface{} {
obj := r.New(p).Elem()
としています。reflect.New(p reflect.Type)
で型情報からインスタンスを作成できます。
Newでは型情報から作成されたインスタンスを指すポインタのValue型のインスタンスが返りますので、
Elem()
としてポインタのさす「中身」のValue型オブジェクトを取得しています。(このあたりややこしいですね)
c言語になじみのある人だとわかると思いますが、アクセス不能なアドレスとかいう話です。メモリ周りのお勉強が必要なのでここでは割愛です。
そのあとはこのstructの型情報からフィールド配列を取得してfor文で回し、
各フィールドの名前を利用してdataから直接値をフィールドにセットするということになります。
バリデーション
バインディングに成功したら、ついでにバリデーションを行っています。
structにある名前でタグ付けされているフィールドのみバリデーションを行っています(ここでは文字数チェックのみ行っています)以下の方法ですね。
field := value.Field(i)
fieldType := valueType.Field(i)
if tagValue, ok := fieldType.Tag.Lookup(PgwayValidationTagName); ok && strings.TrimSpace(tagValue) == "true" {
バリデーションを通過したら、そのインスタンスをもとに該当APIのHandler関数を実行します。
reflect.Value::CallはValue型の配列を返します。複数の戻り値は若干サーバーのインターフェースとしてわかりづらい気がするので、ここでは一つに絞っています。
out := method.Call(in)
switch len(out) {
case 0:
return nil
case 1:
return out[0].Interface()
default:
panic("invalid definition")
}
大まかにはこのような流れです。
おわりです
ということで簡単にオレオレフレームワークを作ってみました。ちょうど自分で作っているプロジェクトで使っ手見ようと思うので、それに合わせてカスタマイズしたりはするかもです。
あと、Markdownは難しいです。