🦝

俺流goのAPIサーバー

5 min read

現在は違うものを使っているのであとでそちらも記事にします。

私流のAPIです。
誰かの参考になればうれしいです。

djangoから入ったためviewという言葉を使いがちですが、controllerだと思ってください。

middleware等を連鎖的に記述できるハンドラー関数。

道中でエラーがreturnされるとそのエラーがレスポンスに、エラーなく終えれば最後まで処理を実行します。

handler.go
package handler

import "net/http"

func Handle(handlers ...func(w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		for _, handler := range handlers {
			if err := handler(w, r); err != nil {
				return
			}
		}
	}
}

例えばhttp.HandleFunc内で以下のように使います。

http.HandleFunc("/blog/post/", handler.Handle(middleware.PostOnlyMiddleware, blog.InsertBlogView))

PostOnlyMiddlewareでエラーを返すとそのエラーがレスポンスに、エラーがない場合blog.InsertBlogViewが実行されます。

レスポンスボディをjsonにする関数

どんな連想配列でも受け入れてしまう関数です。
http.ResponseWriterのWriteメソッドをbyte文字列のjsonにします。

api.go
package api

import (
	"encoding/json"
	"net/http"
)

func JsonResponse(w http.ResponseWriter, dictionary map[string]interface{}) bool {
	data, err := json.Marshal(dictionary)

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return false
	}
	w.WriteHeader(http.StatusOK)
	w.Write(data)
	return true
}

実際に使ってみる

sample/views.go
package sample

import (
	"errors"
	"go_api/api"
	"net/http"
)

var euro = map[string]string{
	"2008": "Spain",
	"2012": "Spain",
	"2016": "Portugal",
}

func SampleView(w http.ResponseWriter, r *http.Request) error {
	query := r.URL.Query()
	year := query.Get("year")

	if winner, ok := euro[year]; ok {
		api.JsonResponse(w, map[string]interface{}{"winner": winner})
		return nil
	} else {
		w.WriteHeader(http.StatusNotFound)
		return errors.New("Not Found")
	}
}
main.go
package main

import (
	"go_api/handler"
	"go_api/sample"
	"net/http"
)

func main() {
	http.HandleFunc("/", handler.Handle(sample.SampleView))
	http.ListenAndServe(":8080", nil)
}

http://localhost:8080/?year=2016
にアクセスすると
{"winner":"Portugal"}となるはずです。

http://localhost:8080/?year=2020
では404 Not Foundとなります。

Middlewareを定義していい感じにする

自分の場合フロントエンドはNextjsを使用しています。
オリジンが違うのでCORSの設定が必要です。

middleware/cors.go
package middleware

import (
	"net/http"
)

func CorsMiddleware(w http.ResponseWriter) error {
	protocol := "http://"
	host := "localhost:3000"
	// こんな感じでローカルかどうか分岐
	// if tools.IsProductionEnv() {
	// 	protocol = "https://"
	// 	host = os.Getenv("FRONT_HOST")
	// }
	w.Header().Set("Access-Control-Allow-Origin", protocol+host)
	w.Header().Set("Access-Control-Allow-Credentials", "true")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, Origin, X-Csrftoken, Accept, Cookie")
	w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT")
	return nil
}

メソッドを限定したいときもあるでしょう。

middleware/methods.go
package middleware

import (
	"errors"
	"net/http"
)

// preflightに対してstatus okを返す
// そうしないとPUTやDELETEのような非単純リクエストが実行されない
func AllowOptionsMiddleware(w http.ResponseWriter, r *http.Request) error {
	if r.Method == "OPTIONS" {
		w.WriteHeader(http.StatusOK)
		return nil
	}
	return nil
}

func PostOnlyMiddleware(w http.ResponseWriter, r *http.Request) error {
	if r.Method == "POST" {
		return nil
	}
	w.WriteHeader(http.StatusMethodNotAllowed)
	return errors.New("METHOD NOT ALLOWED")
}

func GetOnlyMiddleware(w http.ResponseWriter, r *http.Request) error {
	if r.Method == "GET" {
		return nil
	}
	w.WriteHeader(http.StatusMethodNotAllowed)
	return errors.New("METHOD NOT ALLOWED")
}

今回の例でこれを使うと以下のようになります。
まず、corsとpreflightに関しては全ルート共通なので、handlerに入れてしまいます。

handler/handler.go
package handler

import "net/http"

func Handle(handlers ...func(w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		middleware.CorsMiddleware(w)
		middleware.AllowOptionsMiddleware(w, r)
		for _, handler := range handlers {
			if err := handler(w, r); err != nil {
				return
			}
		}
	}
}

このようにすると全ページに対してCorsの設定が有効になり、preflightに対してもstatusOKを返してくれます。

共通でないmiddlewareは以下のように使います。
GETメソッドのみ許可したい場合、main.goを変更します。

main.go
package main

import (
	"go_api/handler"
	"go_api/middleware"
	"go_api/sample"
	"net/http"
)

func main() {
	http.HandleFunc("/", handler.Handle(middleware.GetOnlyMiddleware, sample.SampleView))
	http.ListenAndServe(":8080", nil)
}

こうすることで、GET以外のメソッドに対して405エラーを返してくれます。

終わりに

最近クリーンアーキテクチャを学んでいます。
クリーンアーキテクチャを踏襲しつつ、さらにinterfaceを使ったもっといい感じのができたらそちらも投稿しようと思っています。

今回のものはこちらにまとめてあります。

https://github.com/maru44/go_api