こんにちわ
グローバルセンス株式会社のskanehiraです。
最近は仕事と資格勉強でなかなか記事を書く気になれず、久々になってしまいました。
資格(Java Gold)は無事に取得したので、肩の荷が降りて伸び伸びと実装の勉強をしています(笑)
さて、今回のテーマはGoでFWを使わず、簡単なapi serverを実装してみました。
それについて書いていきます。
このテーマにした理由
最近の業務はGoでのAPI実装と試験がメインなので、実装学習の一環として
Goを使ったapi serverをテーマにしました。
今まではFWを使っていたので、断片的にイメージとして骨組みは頭にありましたが、
改めて、0から作るとなるといろいろ考えないと行けない事があれこれと出てきました。
クオリティは低く、妥協も多々ありますが、
とりあえず動くモノは作れたので、記事に記憶を残そうと思います。
改善したいところはいろいろと…
簡単なAPIサーバーの概要
ユーザ登録・取得・更新・削除を一通りできるAPIを作りました。
DBは使っていないため、オンメモリです。
サーバ止まったら消えます(ひいいいい)
出来上がったものはこちらに上げました。
go run $(ls -l *.go)
するか、go build .
するかで、動かせます。
正常系の動作確認結果はこんな感じになります。
# ユーザ作成
skanehira:~/Dev/Go/gopath/src/sample_api (master *=) $ curl -s -X POST -H "Content-type:application/json" http://localhost:8080/users -d '{"name":"gollira","password":"gollira_suki","description":"test user"}' | jq .
{
"id": "36c27e34-b700-49f9-b8d0-400b54f31aba",
"name": "gollira",
"password": "gollira_suki",
"description": "test user",
"create_at": "2018-08-07T00:38:39.451740022+09:00",
"update_at": "2018-08-07T00:38:39.451740022+09:00"
}
skanehira:~/Dev/Go/gopath/src/sample_api (master *=) $ curl -s -X POST -H "Content-type:application/json" http://localhost:8080/users -d '{"name":"neko","password":"neko_suki","description":"test user2"}' | jq .
{
"id": "a65666c3-d34c-43d6-b47e-70b200c1ef95",
"name": "neko",
"password": "neko_suki",
"description": "test user2",
"create_at": "2018-08-07T00:38:45.255705662+09:00",
"update_at": "2018-08-07T00:38:45.255705662+09:00"
}
# ユーザ取得
skanehira:~/Dev/Go/gopath/src/sample_api (master *=) $ curl -s http://localhost:8080/users?id=a65666c3-d34c-43d6-b47e-70b200c1ef95 | jq .
{
"id": "a65666c3-d34c-43d6-b47e-70b200c1ef95",
"name": "neko",
"password": "neko_suki",
"description": "test user2",
"create_at": "2018-08-07T00:38:45.255705662+09:00",
"update_at": "2018-08-07T00:38:45.255705662+09:00"
}
# ユーザ一覧
skanehira:~/Dev/Go/gopath/src/sample_api (master *=) $ curl -s http://localhost:8080/users | jq .
[
{
"id": "a65666c3-d34c-43d6-b47e-70b200c1ef95",
"name": "neko",
"password": "neko_suki",
"description": "test user2",
"create_at": "2018-08-07T00:38:45.255705662+09:00",
"update_at": "2018-08-07T00:38:45.255705662+09:00"
},
{
"id": "36c27e34-b700-49f9-b8d0-400b54f31aba",
"name": "gollira",
"password": "gollira_suki",
"description": "test user",
"create_at": "2018-08-07T00:38:39.451740022+09:00",
"update_at": "2018-08-07T00:38:39.451740022+09:00"
}
]
# ユーザ更新
skanehira:~/Dev/Go/gopath/src/sample_api (master *=) $ curl -s -X PUT -H "Content-type:application/json" http://localhost:8080/users?id=a65666c3-d34c-43d6-b47e-70b200c1ef95 -d '{"name":"neko_kawaii","password":"neko_suki","description":"test user neko suki"}' | jq .
{
"id": "a65666c3-d34c-43d6-b47e-70b200c1ef95",
"name": "neko_kawaii",
"password": "neko_suki",
"description": "test user neko suki",
"create_at": "2018-08-07T00:40:05.405758877+09:00",
"update_at": "2018-08-07T00:40:05.405758877+09:00"
}
# ユーザ削除
skanehira:~/Dev/Go/gopath/src/sample_api (master *=) $ curl -s -X DELETE http://localhost:8080/users?id=a65666c3-d34c-43d6-b47e-70b200c1ef95 | jq .
{
"code": 200,
"message": "Success delete user=[a65666c3-d34c-43d6-b47e-70b200c1ef95]"
}
skanehira:~/Dev/Go/gopath/src/sample_api (master *=) $ curl -s http://localhost:8080/users | jq .
[
{
"id": "36c27e34-b700-49f9-b8d0-400b54f31aba",
"name": "gollira",
"password": "gollira_suki",
"description": "test user",
"create_at": "2018-08-07T00:38:39.451740022+09:00",
"update_at": "2018-08-07T00:38:39.451740022+09:00"
}
]
skanehira:~/Dev/Go/gopath/src/sample_api (master *=) $
躓いた部分
- パッケージをどのように分けるか
- Requestはどうやって振り分けるか(routing)
- Request Bodyをどのように取得するか
- Responseをどの様に返すか
- error処理して、どう返すか
- validation(parameter、request body)をどこで、どこまでやるか
パッケージをどのように分けるか
当初は以下のパッケージ分けを考えてました。
api/
├ handler/
│ └ userHandler.go
└ model/
│ └ user.go
└ common/
│ └ error.go
└ server/
└ server.go
が、数ファイルしかないのでパッケージ分けしなくて良いやと思ってやめました。
Requestはどうやって振り分けるか(routing)
GoのWebFWはEchoしか使ったがなく、Echoでは以下のように、
URIごとに、Handlerを登録することができます。
登録されたHandlerは対象のURIにアクセスしたときに呼び出される、
という仕組みになっています。
e.POST("/users", saveUser)
e.GET("/users/:id", getUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
Goの標準httpパッケージだと以下の様に登録します。
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
基本的にEchoと同じですが、pathパラメータの場合、
Echoのように自動的に:id
をパラメータに置き換えてくれる機能はなさそうなので、
自前で動的にpathパラメータを取得する処理を書かないと行けない様です。
今回は、そこまで頑張れなかったのでクエリパラメータにしました。
そして、POST,GET,PUT,DELETEをHandler一つにまとめました。
// ServerStart http server start
func ServerStart() {
// regist handler
http.HandleFunc("/users", UserHandler)
// start server
log.Fatal(http.ListenAndServe(":8080", nil))
}
// UserHandler handle user request
func UserHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
Register(w, r)
case http.MethodGet:
Reader(w, r)
case http.MethodPut:
Updater(w, r)
case http.MethodDelete:
Deleter(w, r)
default:
NotFoundResources(w, r)
}
}
/users
にアクセスした場合、r.Method
によって、処理を切り分けています。
routing用のHandlerを作れば良さそうですが、
イマイチやり方わかりませんでした。
エンドポイントが増えるたびに、
同じ様にHandlerを書かないと行けないのが非効率なので、
このやり方はあまりよろしくないですが、一旦このような形にしました。
Request Bodyをどのように取得するか
基本的にAPIといえば、データのやり取りはJSONで行うのが主流と思います。
Request Bodyからjsonを取得して、構造体に入れるには以下のステップが必要です。
- 構造体にjsonタグをつける
-
json.NewDecoder()
にBodyと構造体ポインタを渡す
json.NewDecoder()
では、構造体のタグをもとに、
データの紐づけを行うようです。
なので、タグがないとデータがバインドされないので注意する必要があります。
// User user data struct
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Password string `json:"password"`
Description string `json:"description"`
CreatedAt time.Time `json:"create_at"`
UpdatedAt time.Time `json:"update_at"`
}
そして、Request BodyからPOST,PUTデータを取得ため、関数を用意しました。
// request body to struct
func requestBodyToStruct(r *http.Request, data interface{}) error {
if err := json.NewDecoder(r.Body).Decode(data); err != nil {
return NewError(http.StatusBadRequest, err.Error())
}
return nil
}
jsonパッケージでUnmarshal()
というのがありますが、
Bodyを処理する時はNewDecoder
を使うのが一般的の様です。
何が違うのか分かっていないので、今度じっくり見てみようと思います。
関数を使用する時は、
第1引数にRequest、第2引数に、jsonデータを入れたい構造体をポインタで渡します。
newUser := User{}
err := requestBodyToStruct(r, &newUser)
Responseをどの様に返すか
jsonデータを構造体に入れるのと同じ要領で、
構造体に入れたデータをjson形式に変換してResponse Bodyに書き込む必要があります。
ステップとして以下になります。
- 構造体のデータ[]byte型に変換
- []byte型データをResponse Bodyに書き込む
以下の関数を用意しました。
// struct to response body
func structToResponseBody(data interface{}) []byte {
json, err := json.Marshal(&data)
if err != nil {
return []byte(err.Error())
}
return json
}
上関数を使い、構造体をstructToResponseBody()
に渡して、
[]byte型データをResponse Bodyに書き込みます。
Response Bodyに書き込む処理は共通なので、この様に関数に切り出しました。
// creat http response
func newHTTPResponse(w http.ResponseWriter, code int, body interface{}) {
w.WriteHeader(code)
w.Write(structToResponseBody(body))
return
}
error処理して、どう返すか
Goの標準パッケージerrors
を使用して、error型を返すのが一般的かと思います。
今回は、少しカスタマイズしようと思い、構造体を用意しました。
Response Bodyをjsonで返せるように構造体のフィールドにタグをつけます。
自前で定義したErrorMessage
をerror型
として扱いたいので、Error()
を実装しました。
Javaではinterface
をimplements
(実装)することで、ポリモフィズムを実現しますが、
Goではinterfaceで定義した関数を実装することでポリモフィズムを実現します。
Error()
を実装することで、error型として扱えるようになるため、
ErrorMessage
をerror型で返すことができ、err != nil
というふうに比較できる様になります。
// ErrorMessage error struct
type ErrorMessage struct {
Code int `json:"code"`
Message string `json:"message"`
}
// NewError new error
func NewError(code int, message string) ErrorMessage {
return ErrorMessage{
Code: code,
Message: message,
}
}
// implements error interface
func (e ErrorMessage) Error() string {
return fmt.Sprintf("code=[%d] message=[%s]", e.Code, e.Message)
}
こういうふうに使います。
// request body to struct
func requestBodyToStruct(r *http.Request, data interface{}) error {
if err := json.NewDecoder(r.Body).Decode(data); err != nil {
return NewError(http.StatusBadRequest, err.Error())
}
return nil
}
validation(parameter、request body)をどこで、どこまでやるか
本当は入力に対してすべてvalidationすべきですが、
今回は手を抜きまくって、リクエストヘッダとボディの長さ、user idで使用しているuuid v4だけvalidationしました。
// uuid v4 regexp
var validUUID = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
// ・・・・
// check post and put request header
func checkRequestHeader(r *http.Request) error {
if r.Method == http.MethodPost || r.Method == http.MethodPut {
code := http.StatusBadRequest
if r.Header.Get("Content-type") != "application/json" {
return NewError(code, "Content-type is not application/json")
}
if r.ContentLength == 0 {
return NewError(code, "Request body length is 0")
}
}
return nil
}
本来なら、こんなUser構造体にvalidation関数を実装して、
そこでリクエストメソッドごとにバリデーションを展開するが望ましいと思います。
大体こんな感じかと。
// Validation user data validation
func (u *User) Validation(r *http.Request) error {
switch r.Method {
case http.MethodPost:
// do something
case http.MethodPut:
// do something
}
return nil
}
まとめ
実装してみたもモノの、改善出来るところはいくつかあります。
特にRoutingやRequest、Responseあたりです。
既存のFWはどの様に実装しているのかを見て勉強した方が良いなと思いました。
やっぱFWは偉大だったのはよくわかりました。
感謝感謝…
全体を通して感じたこと
改めてFWとの付き合い方を考えさせられました。
今の時代では、言語毎にいろんなFWがあります。
たくさんあるがゆえ、モノやサービスをそれなりのクオリティで作れてしまいます。
ましては、AIによるhtml/css自動コーディングの研究も進められているので、
どんどん時代は便利になっていきます。
便利さとは反面に、エンジニアのスキルは両極端化しやすくなると思います。
FWを使わないとモノを作れないエンジニアも出てくるかもしれません。
FWに頼り切りになるのは良くないけど、使いこなせる様になれば強い武器になると思います。