API Gateway向けの AWS Lambda のハンドラーをGoで作成する場合、普通に書くとこのように、main関数から特定のシグネチャの関数を lambda.Start で呼び出す。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-handler.html

package main
 
import (
        "fmt"
        "context"
        "github.com/aws/aws-lambda-go/events"
        "github.com/aws/aws-lambda-go/lambda"
)
 
func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	return events.APIGatewayProxyResponse{
		Body:       fmt.Sprintf("Hello, %v", request.QueryStringParameters["name"]),
		StatusCode: 200,
	}, nil
}
 
func main() {
        lambda.Start(handler)
}

API GatewayのパスごとにFunctionを紐づけている場合に、ヘッダーからトレース用の情報を取得したり、ユーザー情報を取得したりといった各ハンドラー共通で実行する処理を書きたい。

API Gateway --- /hello HelloFunction └----- /bye ByeFunction

hello/main.go

package main
 
import (
	"context"
	"fmt"
 
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)
 
func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	traceId := request.Headers["X-Amzn-Trace-Id"]
	fmt.Println(traceId)
 
	return events.APIGatewayProxyResponse{
		Body:       fmt.Sprintf("Hello, %v", request.QueryStringParameters["name"]),
		StatusCode: 200,
	}, nil
}
 
func main() {
	lambda.Start(handler)
}

bye/main.go

package main
 
import (
	"context"
	"fmt"
 
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)
 
func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	traceId := request.Headers["X-Amzn-Trace-Id"]
	fmt.Println(traceId)
 
	return events.APIGatewayProxyResponse{
		Body:       fmt.Sprintf("Bye, %v", request.QueryStringParameters["name"]),
		StatusCode: 200,
	}, nil
}
 
func main() {
	lambda.Start(handler)
}

各ハンドラーのコードに同じ処理を書くのは、数が増えたときに間違えやすくなるので、なんとかしたい。

http handlerのミドルウェア

net/httpでwebサーバーを作るときに、各http handlerに共通で実行したい処理を実装するパターンの一つとして、ミドルウェアを書くことがある。

参考 HTTP Middleware の作り方と使い方 - 技術メモ

これは引数に http.Handler をとって http.Handler を返す関数で、ハンドラーの前後に処理を挟み込むのと、ミドルウェア自身をミドルウェアでラップすることができる。

func logging(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Request: %v\n", r.URL.String())
    next.ServeHTTP(w, r)
  })
}

これをさらにwrapして、設定値を渡すような書き方もできる。echoのミドルウェアはこのような実装になっている。

type HttpMiddlewareFunc func(next http.Handler) http.Handler
 
func LoggingMiddleware(debug bool) HttpMiddlewareFunc {
    return func (next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if debug {
                fmt.Printf("Request: %v\n", r.URL.String())
            }
            next.ServeHTTP(w, r)
        })
    }
}

lambda handlerのミドルウェアに応用する

同様の方法でミドルウェアを書くとこんな感じになる。

type LambdaHandlerFunc func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
type LambdaMiddlewareFunc func(next LambdaHandlerFunc) LambdaHandlerFunc
 
func LoggingMiddleware(debug bool) HttpMiddlewareFunc {
    return func(next LambdaHandlerFunc) LambdaHandlerFunc {
        return LambdaHandlerFunc(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
            if debug {
                fmt.Printf("Request: %v\n", request)
            }
 
            return next(ctx, request)
        })
    }
}

実用的な例としては以下のようになる。

package middleware
 
import (
	"context"
	"net/http"
 
	"github.com/aws/aws-lambda-go/events"
)
 
type LambdaHandlerFunc func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
type LambdaMiddlewareFunc func(next LambdaHandlerFunc) LambdaHandlerFunc
 
// ApplyMiddlewares
func ApplyMiddlewares(handler LambdaHandlerFunc) LambdaHandlerFunc {
	middlewares := []LambdaMiddlewareFunc{
		Header(),
		Auth(),
	}
 
	for i := len(middlewares) - 1; i >= 0; i-- {
		handler = middlewares[i](handler)
	}
 
	return handler
}
 
// Header contextにデフォルトのrequest/response headerをセットする
func Header() LambdaMiddlewareFunc {
	return func(next LambdaHandlerFunc) LambdaHandlerFunc {
		return func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
 
			resHeaders := http.Header{
				"Content-Type": []string{"application/json"},
			}
			ctx = SetResponseHeader(ctx, resHeaders)
 
			return next(ctx, request)
		}
	}
}
 
func Auth() LambdaMiddlewareFunc {
	return func(next LambdaHandlerFunc) LambdaHandlerFunc {
		return func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
			authHeader := request.Headers["Authorization"]
			// 認証処理
 
			ctx = SetUser(ctx, user)
 
			return next(ctx, request)
		}
	}
}
 
// ----
// ctxにセット/取得する処理
// ----
 
type ctxKey string
 
const (
	ctxKeyUser      ctxKey = "ctxKeyCustomerUser"
	ctxKeyResHeader ctxKey = "ctxKeyResHeader"
)
 
func SetResponseHeader(ctx context.Context, headers http.Header) context.Context {
	ctx = context.WithValue(ctx, ctxKeyResHeader, headers)
	return ctx
}
 
func ResponseHeaderFromContext(ctx context.Context) http.Header {
	if v, ok := ctx.Value(ctxKeyResHeader).(http.Header); ok {
		return v
	}
	return http.Header{}
}
 
func SetUser(ctx context.Context, user *User) context.Context {
	ctx = context.WithValue(ctx, ctxKeyUser, user)
	return ctx
}
 
func UserFromContext(ctx context.Context) *User {
	if v, ok := ctx.Value(ctxKeyUser).(*User); ok {
		return v
	}
	return nil
}
 

hello/main.go

func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	// ...Handlerの処理
}
 
func main() {
	lambda.Start(ApplyMiddlewares(handler))
}