本記事では、まずRESTとは何かを説明し、Go言語でのREST APIの実装例を紹介します。RESTについての理解があやふやであるという方も、REST APIの実装を見ることでRESTについてより深い理解が得られるでしょう。この記事で使用するソースコードはこちらに載せてあります。
それでは、まずRESTとは何かというところから説明していきます。
##RESTとは?
まず、RESTを一言で言うといくつかの原則に基づいた、Webシステムを設計する際の設計モデルです。ちなみにRESTとは、Representational State Transferの略です。
なので、RESTとは何か、と問われた時には「Webシステムを設計する際の設計モデルのことです。」と回答し、それからその原則について説明していくことになります。
###RESTの原則
RESTには、いくつかの原則があり、この原則がRESTの本質部分です。この原則を遵守したシステムをRESTfulであると言います。
- ステートレスであること
- インターフェースの統一(HTTPメソッドの利用)がされていること
- 全てのリソースが他のリソースと被らない一意なURIを持っていること
- 表示したリソースにリンクなどを載せて別のリソースへ辿れるようになっていること
これらの原則に則ったWebシステムをRESTfulなWebシステムであるいい、RESTfulなWebシステムのAPIを特にREST APIと呼びます。以下の実装例では、
- インターフェースの統一(HTTPメソッドの利用)がされていること
- 全てのリソースが他のリソースと被らない一意なURIを持っていること
この二つの原則が実際に適用されています。
##REST APIのGo言語での実装例
RESTについて言葉で説明をしましたので、実際にRESTfulなAPIのGo言語での実装例を見て更に理解を深めていきましょう。
※今回は簡略化のためにDBを使いません。ソースコードにデータをベタ書きします。
###まずはリソースを定義
まずはリソースを定義します。今回はDBのデータではなくソースコードにベタ書きしたデータを、JSONで扱えるようにした構造体に入れてリソースを定義します。
ちなみに、リソースとは画像やデータなどプログラムが使用するデータのことです。この場合、リソースとはJSON形式で定義したallArticlesの配列の中のデータがリソースになります。encoding/jsonパッケージでは、タグを利用してJSON内のキー名と各変数の結びつけを行っています。 なお、タグを省略した場合は変数名と全く同じ名前のキーが自動的に結び付けられます。
ここでは、IDはjsonの中でidとして扱われることを示しています。
type article struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
}
type allArticles []article
var articles = allArticles{
{
ID: "12",
Title: "Introduction to golang",
Description: "Go言語の基本",
},
{
ID: "13",
Title: "Introduction to Algorithm",
Description: "アルゴリズムの基本",
},
{
ID: "14",
Title: "Introduction to Programming",
Description: "プログラミングの基本",
},
}
ここではリソースをarticlesとして表現しています。RESTではリソースを名詞を使って表し、KISSの原則に従うと名詞は常に複数形を使うというのが一般的となっているようですので、Web APIを設計する時のために覚えておくと良いでしょう。
###GETメソッドを定義
これでリソースの定義ができました。次にGETメソッドを定義してこれらのリソースを全て取得してみます。
func getArticles(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(articles) //JSONにエンコードしたarticlesをレスポンスボディ(w)に書き込む
}
既に触れましたが、RESTfulなWebシステムでは全てのリソースが他のリソースと被らない一意なURIを持っています。よって、既に定義したarticlesというリソースにもURIを与えなければなりません。それをしているのが以下の処理です。
func main() {
http.HandleFunc("/articles", handleAllArticlesRequest)
http.ListenAndServe(":8081", nil)
}
func handleAllArticlesRequest(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getArticles(w, r)
default:
http.Error(w, r.Method+" method not allowed", http.StatusMethodNotAllowed)
t := time.NewTicker(100)
for range t.C {
t.Stop()
}
}
}
これでhttp://localhost:8081/articles にリクエストが来た時にhandleAllArticlesRequestが呼び出されるように紐付けを行い、GETメソッドが来た時には先ほど定義したgetArticlesが呼び出されるようになりました。
###GETリクエストを送信
ここまでできたところで実際にこのプログラムを実行してPostmanでリクエストを送信してみましょう。
ちゃんとソースコード上で定義したリソースが返ってきているのが確認できたと思います。
###POSTメソッドを定義
では次にPOSTリクエストを送信してデータの追加をできるようにしてみましょう。
func addArticle(w http.ResponseWriter, r *http.Request) {
var article article
json.NewDecoder(r.Body).Decode(&article)
articles = append(articles, article)
json.NewEncoder(w).Encode(articles)
}
リクエストボディに含まれるデータをarticle型の変数に格納しています。ここで&article
とアンパサンドを使っているのはそうしなければ値渡しが起こってしまい、後の処理でarticleの中身が空になってしまうからです。
実行するとこのようにちゃんとデータが追加されているのが確認できます。
###リソースを一つずつ操作する
ここまではarticlesというリソースを集合のように扱ってきました。しかし、articleのIDが12のものを参照したい、という時も当然あるはずです。この記事では、http://localhost:8081/articles/12 このようにIDをURIで指定し操作する実装例を紹介します。
####リソースをIDで指定してGETリクエストで取得する
IDで取得するため、URIに含まれたIDをどうにかして取得する必要があります。getArticleメソッドでは、idをURIから取得してarticlesの中から同じidを持つものを探してレスポンスボディに埋め込んでいます。
func getArticle(w http.ResponseWriter, r *http.Request) {
eventID := r.URL.Path //URIを返す。articles/test/13 にアクセスしたら articles/test/13を返す。
fmt.Printf("r : %+v\n", r)
fmt.Println("eventID: ", eventID)
for _, article := range articles {
if article.ID == eventID {
json.NewEncoder(w).Encode(article)
break
}
}
}
URIはr.URL.Path
のようにして取得することができますが、このやり方だとarticles/12というURIへのリクエストにメソッドを紐付けると、r.URL.Path
はarticles/12を返してしまいます。欲しいのは12の部分なので、URIの末尾を取得するためにはハンドラ側でちょっとした工夫が必要です。
func main() {
http.HandleFunc("/articles", handleAllArticlesRequest)
http.Handle("/articles/", http.StripPrefix("/articles/", http.HandlerFunc(handleSingleArticleRequest)))
http.ListenAndServe(":8081", nil)
}
func handleSingleArticleRequest(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
getArticle(w, r)
case "DELETE":
deleteArticle(w, r)
case "PATCH":
updateArticle(w, r)
default:
http.Error(w, r.Method+" method not allowed", http.StatusMethodNotAllowed)
}
}
このように、http.StripPrefixを使ってURIから/articles/の部分を取り除き、r.URL.Path
でうまく12だけを取り出せるようにしています。
http.HandlerFunc
型にキャストすることでHandlerインタフェースを第二引数にとるStripPrefixメソッドの第二引数に使うことが可能になっています。HandlerインタフェースはServeHTTPを持つインタフェースで、HandlerFuncもServeHTTPを実装しているため、handleSingleArticleRequestをHandlerFuncにキャストすることでHandlerインタフェースを満たすようになり、引数として使えるようになるのです。
//server.go
func StripPrefix(prefix string, h Handler) Handler {
if prefix == "" {
return h
}
return HandlerFunc(func(w ResponseWriter, r *Request) {
if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) {
r2 := new(Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = p
h.ServeHTTP(w, r2)
} else {
NotFound(w, r)
}
})
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
以上がGo言語での標準パッケージのみを用いたRest APIのごく簡単な実装になります。ここまでが理解できれば、PATCH, DELETEなど他のリクエストに対応するメソッドもすんなり理解できるはずです。この記事内では紹介していませんが、他のリクエストに対応するメソッドもソースコード上に書いてありますので参考にしてみてください。
この記事で使用したソースコードはこちらに載せてあります。
それでは、最後までお読みいただきありがとうございました。