Go言語で作るJSON API
今回は以下の記事を参照しながら、RESTful APIを作っていきます。
RESTfulなAPI設計には次の条件を満たす必要があります。
- 適切なステータスコードを返す
- コンテンツヘッダーなどを返送すること
まず初めに、以下のファイルをプロジェクトフォルダに作ります。
それぞれの役割は次の通りです。
- Main.go → main関数をおいて、特定のポートに指定したRouter機能をつける
- Handlers.go → 各endpoint(route)を置いて、特定のURL pathに行った時の処理を設 定する。JSONのencodeなども、ここで処理されます。
- Todo.go → データモデル(扱いたい要素に合わせて)を設定する
- Router.go → Rotes.goファイルにあるroute dataをループ処理して、返す
- Routes.go → routeが持つ情報を保持する
- logger.go → Web Requestのログを取る処理を行う。
- repo.go → DBの代わりに、データ処理を行う。
では、main.goファイルから見ていきます。
main.go
package mainimport (
"log"
"net/ne"
)func main() {router := NewRouter()log.Fatal(http.ListenAndServe(":8080", router))
}
package名はmainに設定します。今回の場合は、全てのファイルともに同じフォルダに入れているので、全て package main
になります。
また、ListenAndServeは指定したアドレス(port番号)と、handler(変数router)をもってHTTP serverをstartさせます。
Handler.go
package mainimport (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http""github.com/gorilla/mux"
)//Index set a default endpoint for the root path
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "welcome!")
}//TodoIndex exports just string "Todo Index"
func TodoIndex(w http.ResponseWriter, r *http.Request) {w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)if err := json.NewEncoder(w).Encode(todos); err != nil {
panic(err)
}
}// TodoShow exports route, coresponding to the passed ID
func TodoShow(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
todoID := vars["todoID"]fmt.Fprintln(w, "Todo show:", todoID)
}// TodoCreate create endpoint to the handlers file
func TodoCreate(w http.ResponseWriter, r *http.Request) {
var todo Todo
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))if err != nil {
panic(err)
}
if err := r.Body.Close(); err != nil {
panic(err)
}
if err := json.Unmarshal(body, &todo); err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(422)
if err := json.NewEncoder(w).Encode(err); err != nil {
panic(err)
}
}t := RepoCreateTodo(todo)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(t); err != nil {
panic(err)
}
}
まず、上のコードの2つの関数TodoIndexとTodoCreateを、それぞれ見ていきます。
//TodoIndex exports just string "Todo Index"
func TodoIndex(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(todos); err != nil {
panic(err)
}
}
コンテンツタイプを返送し、クライアントにjsonを期待するように指示します。 次に、ステータスコードを明示的に設定します。
net/httpサーバーは出力するcontentの型タイプを推測しますが、分かっている今回の様な場合(JSON type)は、設定したほうがいいです。
// TodoCreate create endpoint to the handlers file
func TodoCreate(w http.ResponseWriter, r *http.Request) {
var todo Todo
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))if err != nil {
panic(err)
}
if err := r.Body.Close(); err != nil {
panic(err)
}
if err := json.Unmarshal(body, &todo); err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(422)
if err := json.NewEncoder(w).Encode(err); err != nil {
panic(err)
}
}t := RepoCreateTodo(todo)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(t); err != nil {
panic(err)
}
まずio.LimitReaderを使用して、Requestの本文を開きます。
io.LimitReaderのメリットは、サーバー上の悪意のある攻撃から保護する、膨大なJSONデータを送られるなどを防ぐことが出来ます。
本文を開き終えたら、Todo structにUnmarishal(JSON文字列からstructへ変換)します。 この処理が失敗した場合、適切なステータスコード422で応答するだけでなく、json文字列でエラーを返送します。
これにより、何がうまくいかなかったかを明確に伝えることができるようになります。
最後に、すべてがうまくいったら、ステータスコード201を返します。これは、Entityが正常に作成されたことを意味します。 また、作成した EntityのJSONを返します。これは、client-sideが次の処理に必要とするidを含んでいるために行われます。
Todo.go
package mainimport "time"// Todo serves as a todo-model
type Todo struct {
ID int `json:"id"`
Name string `json:"name"`
Completed bool `json:"completed"`
Due time.Time `json:"due"`
}// Todos is a sliec of Todo
type Todos []Todo
Go言語ではdata modelはstructで実装されます。他の言語のClassの部分が、structで作られるといった感じですね。
最後のTodosタイプは、Todoで作られたデータの集まりをsliceとして扱うことで、処理がしやすくなるためのものになります。
なので、structでdata modelを、 + sliceをセットで考えていきます。
また、JSONを扱う際には、uppercaseの文字列をkeyに使うのはNGになります。
そこで、 json:"id"`
のように、それぞれの要素に付けます。
Router.go
package mainimport (
"net/http""github.com/gorilla/mux"
)// NewRouter exports each routes, according to its path
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)for _, route := range routes {
var handler http.Handler handler = route.HandlerFunc
handler = Logger(handler, route.Name) router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(route.HandlerFunc)
}
return router
}
こちらにあるように、gorilla/muxパッケージに則した記法になります。
routerに、変数routesそれぞれのmethod/path/name/handlerを持たして、全てのrouter情報を返しています。
Routes.go
package mainimport (
"net/http"
)// Route serves as a model of each routes
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}// Routes is a slice of Route
type Routes []Routevar routes = Routes{
Route{
"Index",
"GET",
"/",
Index,
},
Route{
"TodoIndex",
"GET",
"/todos",
TodoIndex,
},
Route{
"TodoShow",
"GET",
"/todos/{todoID}",
TodoShow,
},
Route{
"TodoCreate",
"POST",
"/todos",
TodoCreate,
},
}
Todo.goの時と同様に、structタイプとsliceタイプをセットで設定して、Routeの情報を設定しています。メソッドには特定のアクション(GET, POST, DELETE等)を設定することができます。
この4つ以外のroute、つまりここで定義されないrouteを見ようとしても、404 エラーが返されます。
因みに、Routeタイプのある要素のタイプ↓(下の関数)は
http.HandlerFunc
通常の関数を、HTTPハンドラとして使用できるようにするためのアダプタです。
今回は、Handler.goファイルで設定された各関数が、それに当たります。
logger.go
package mainimport (
"log"
"net/http"
"time"
)//Logger wrap the passed handler with logging and timing
func Logger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inner.ServeHTTP(w, r)log.Printf(
"%s\t%s\t%s\t%s",
r.Method,
r.RequestURI,
name,
time.Since(start),
)
})
}
Logger関数はhttp.Handlerを返り値にもつので、返り値に更に機能(log追跡)を、http.HandlerFuncを用いて付けています。
なので、http.Handlerが呼ばれる度に、このLogger関数も呼ばれます。メソッドでみると次のようになりますね。
http.HandleFunc("/path", Logger(handleThing))
handleThingには、IndexやTodoIndex, TodoShowなど各endpointsが入ります。
repo.go
package mainimport "fmt"var currentID int
var todos Todosfunc init() {
RepoCreateTodo(Todo{Name: "Write presentation"})
RepoCreateTodo(Todo{Name: "Host meetup"})
}func RepoFindTodo(id int) Todo {
for _, t := range todos {
if t.ID == id {
return t
}
}
return Todo{}
}func RepoCreateTodo(t Todo) Todo {
currentID++
t.ID = currentID
todos = append(todos, t)
return t
}func RepoDestroyTodo(id int) error {
for i, t := range todos {
if t.ID == id {
todos = append(todos[:i], todos[i+1:]...)
return nil
}
}
return fmt.Errorf("couldnt find Todo with id of %d to delete", id)
}
DB機能の代わりに、ここでは検索・作成・削除機能が設定されています。
上のファイル全てを書き終えたら、 go build
をプロジェクトのフォルダの上で打ち、binary fileをつくります。
そのファイル ./main
をコマンドでうち、http://localhost:8080/todosを開いた時にJSONデータが見れれば成功です。
また、次のコマンドを打ってJSONをPOSTした時に、endpointが新しく作られるか試します。
curl -H “Content-Type: application/json” -d ‘{“name”:”New Todo”}’ http://localhost:8080/todos
結果は、このようになります。
[
{
"id": 1,
"name": "Write presentation",
"completed": false,
"due": "0001-01-01T00:00:00Z"
},
{
"id": 2,
"name": "Host meetup",
"completed": false,
"due": "0001-01-01T00:00:00Z"
},
{
"id": 3,
"name": "New Todo",
"completed": false,
"due": "0001-01-01T00:00:00Z"
}
]
次にやることリスト
- version control用に、routeに/v1/をつける。
Router.goファイルのNewRouterに次のコードを入れる。
router := router.PathPrefix("/v1/").Subrouter()
- Authentication
JSON web tokensを用います。 - html etagをつける
- APIのテストをする。