概要
Goの net/http パッケージと httprouter パッケージでAPIサーバを実装します。
net/http とは
net/http パッケージは,GoでHTTPクライアントとHTTPサーバを実装するためのパッケージです。今回の記事では,HTTPサーバを実装するために使用します。
httprouter とは
HTTPリクエストをルーティングするためのパッケージです。
このHTTPルータは,軽量でパフォーマンスが良いことが特徴で,ルーティング先の数が増加したり非常に長いパスのリクエストに対応するときでも,高パフォーマンスで処理できます。(READMEに書いてあった)
httprouterを使わないルーティング方法もありますが,本記事では,httprouterを使ってルーティングを実施します。
実装
APIサーバのサンプルを実装するために,本の情報を取得する以下の2種類のAPIを実装します。
(簡単のため,DBへのアクセスは行いません。)
- GET /books API
- GET /books/:id API
構成
- 構成はレイヤードアーキテクチャを意識している。ただし,DBアクセスする Infrastracture 層は作成しない。
- handler は User Interface 層でユーザからのAPIコールを処理する。
- usecase は Usecase 層(application 層)ユースケースを実現する(ここでは,本の一覧を取得するユースケース)。
/src
┣ /handler
┃ ┗ book_handler.go
┣ /usecase
┃ ┗ book_usecase.go
┗ main.go
ユースケースの実装
まずは,本の情報を取得するユースケースを実装します。
デモのため,サンプルデータを返します。
package usecase
type Book struct {
ID string `json:"id"`
Name string `json:"name"`
}
// デモ用サンプルデータ
var sampleBookList = []Book{
{
ID: "1",
Name: "Go言語プログラミングエッセンス",
},
{
ID: "2",
Name: "Go言語によるWebアプリケーション開発",
},
}
// GET /books 用のユースケース
func GetBookList() []Book {
// デモ用データを返す
return sampleBookList
}
// GET /books/:id 用のユースケース
func GetBook(id string) *Book {
// IDで指定した本が存在した場合はその本を返す
for _, book := range sampleBookList {
if book.ID == id {
return &book
}
}
// 存在しなかった場合は nil を返す
return nil
}
ユーザインターフェースの実装
ユーザからAPIコールを受けユースケース層に渡す処理と,クライアントにデータを返す処理を実装します。
関数の引数には http.ResponseWriter
・*http.Request
・httprouter.Params
を渡しています。
パスパラメータの取得には,引数のhttprouter.Params
を使うことができ,params.ByName(<パラメータ名>)
で取得できます。
ちなみに,クエリパラメータは,req.URL.Query().Get(<パラメータ名>)
で取得できます。
また,json:"<tag>"
を指定することで,APIのレスポンスのフィールドを変更できます。タグを指定しなかった場合は,構造体のフィールドがそのままレスポンスのフィールドになります。
import (
"encoding/json"
"net/http"
"demoapi/src/usecase"
"github.com/julienschmidt/httprouter"
)
type ErrorResponse struct {
Message string `json:"message"`
}
type BookListResponse struct {
Books []BookResponse `json:"books"`
}
type BookResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
// GET /books のリクエストを処理する
func GetBookList(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
bookList := usecase.GetBookList()
// レスポンスを生成する
response := BookListResponse{Books: []BookResponse{}}
for _, book := range bookList {
response.Books = append(response.Books, BookResponse{ID: book.ID, Name: book.Name})
}
// レスポンスを encode する
err := json.NewEncoder(writer).Encode(response)
if err != nil {
return
}
}
// GET /books/:id のリクエストを処理する
func GetBook(writer http.ResponseWriter, req *http.Request, params httprouter.Params) {
// Path パラメータのidを取得
id := params.ByName("id")
// ID が指定されていない場合は 404 Not Found を返す
if id == "" {
writer.WriteHeader(http.StatusNotFound)
json.NewEncoder(writer).Encode(ErrorResponse{
Message: http.StatusText(http.StatusNotFound),
})
return
}
book := usecase.GetBook(id)
// 本が存在しない場合は 404 Not Found を返す
if book == nil {
writer.WriteHeader(http.StatusNotFound)
json.NewEncoder(writer).Encode(ErrorResponse{
Message: http.StatusText(http.StatusNotFound),
})
return
}
// レスポンスを生成する
response := BookResponse{ID: book.ID, Name: book.Name}
// レスポンスを encode する
err := json.NewEncoder(writer).Encode(response)
if err != nil {
return
}
}
APIサーバの設定
router.GET()
では,クライアントがアクセスするパスと実行される handler の対応付けを行います。
ポートは8080に設定しました。
import (
"net/http"
"demoapi/src/handler"
"github.com/julienschmidt/httprouter"
)
func main() {
router := httprouter.New()
// ユーザが http://<domain>/ にアクセスすると,GetBookList が実行される
router.GET("/", handler.GetBookList)
// ユーザが http://<domain>/ にアクセスすると,GetBook が実行される
router.GET("/books/:id", handler.GetBook)
http.ListenAndServe(":8080", router)
}
実行
以下を実行します。これでAPIサーバが起動します。
go mod init
go mod tidy
go run main.go
localhost の 8080 番ポートにアクセスすると,レスポンスが返ってくるのが確認できました。
$ curl -i http://localhost:8080/books
{"books":[{"id":"1","name":"Go言語プログラミングエッセンス"},{"id":"2","name":"Go言語によるWebアプリケーション開発"}]}
$ curl http://localhost:8080/books/1
{"id":"1","name":"Go言語プログラミングエッセンス"}
$ curl http://localhost:8080/books/2
{"id":"2","name":"Go言語によるWebアプリケーション開発"}
$ curl http://localhost:8080/books/3
{"message":"Not Found"}
まとめ
net/http と httprouter でAPIサーバを実装してみました。
誰かの参考になれば幸いです。