🌐

GinでRESTful APIを実装する

2022/11/20に公開

今回は、GoチュートリアルのTutorial: Developing a RESTful API with Go and Ginをやっていく。

このチュートリアルでは、GoとGin(GoのWebフレームワーク)を使用して、RESTful APIを作成する。

API エンドポイントの設計

ビンテージレコードのレコードを販売するストアへのアクセスを提供するAPIを構築する。
そのため、クライアントがユーザのアルバムを取得および追加できるエンドポイントを提供できるようようにする。

このチュートリアルで作成するエンドポイントは次の通り。

  • /albums
    • GET - アルバムのリストをJSONで返す。
    • POST -JSONで送信されたリクエストデータから新しいアルバムを追加する。
  • /albums/:id
    • GET - IDでアルバムを取得し、アルバムデータをJSONとして返す。

準備

ディレクトリの準備

web-service-ginというディレクトリを用意して、モジュールの初期化を行う。

$ mkdir web-service-gin
$ cd web-service-gin
$ go mod init example.com/web-service-gin
go: creating new go.mod: module example.com/web-service-gin

データの準備

今回はチュートリアルなのでデータはメモリ内で保存する。通常であればデータベースなどを使う。
main.goファイルを作成する

main.go
package main

type album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

var albums = []album{
	{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
	{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
	{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

構造体albumを宣言し、album型のスライスに事前データを入れて変数albumsに代入する。

全てのアイテムを返すハンドラの作成

ロジックを考える

GET /albumsを実行したときに全てのアルバムをJSONで返すエンドポイントを実装する。

そのためには、次のような準備を行う。

  • レスポンスを準備するロジックの実装
  • リクエストパスをロジックにマッピングする実装

実装

main.goに実装していく

main.go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

var albums = []album{
	{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
	{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
	{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func getAlbums(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, albums)
}

func main() {
	router := gin.Default()
	router.GET("/albums", getAlbums)

	router.Run("localhost:8081")
}

gin.Contextはリクエスト情報が格納されており、JSONを検証してシリアライズする。
Goの組み込みのcontextパッケージとは異なる。
Context.IndentedJSONで構造体をJSON にシリアル化し、それをレスポンスに追加する。
http.StatusOKnet.httpパッケージから定数StatusOKを引っ張ってきて、返している。中身はHTTPのステータスコード。

main部分は、リクエストパスに関する処理。

コードの実行

外部からGinモジュールを取得するために、まずはコード内の依存関係を取得してくる。

$ go get .

実行する

$ go run .

デバッグメッセージが表示されるが、サービスが起動して指定したポートでリッスンしたら成功。
curlでアクセスしてみる。

curl http://localhost:8081/albums
[
    {
        "id": "1",
        "title": "Blue Train",
        "artist": "John Coltrane",
        "price": 56.99
    },
    {
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
    },
    {
        "id": "3",
        "title": "Sarah Vaughan and Clifford Brown",
        "artist": "Sarah Vaughan",
        "price": 39.99
    }
]

/albumsエンドポイントが正しく機能していることがわかる。

新しいアイテムを追加するハンドラの実装

ロジックを考える

次は、POST /albumsでリクエストボディで渡されたアルバム情報を既存のアルバムデータに追加していく。

  • 新しいアルバムを既存のリストに追加するロジック
  • POSTリクエストをロジックにルーティングするための実装

実装

アルバムを追加する処理と、ルーティング処理をmain.goに追加していく。
それ以外の部分は変更がないため、省略。

main.go
//...それ以外は変更なしなので省略

func postAlbums(c *gin.Context) {
	var newAlbum album

	if err := c.BindJSON(&newAlbum); err != nil {
		return
	}

	albums = append(albums, newAlbum)
	c.IndentedJSON(http.StatusCreated, newAlbum)
}

func main() {
	router := gin.Default()
	router.GET("/albums", getAlbums)
	router.POST("/albums", postAlbums)

	router.Run("localhost:8081")
}

context.BindJSONを使って、変数newAlbumにリクエストボディをバインドさせる。
JSONから初期化された構造体albumのデータをalbumsスライスに追加していく。
追加したアルバム情報をインデントされたJSONで201ステータスコードと共にレスポンスで返す。
mainに、POSTリクエストしたときにpostAlbumsにルーティングする処理を追加する。

コードの実行

一旦サーバを停止し、再度コードを実行する。

$ go run .

curlでPOSTリクエストを送ってみる。

$ curl http://localhost:8081/albums \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id":"4", "title":"The Modern Sound of Betty Carter", "artist":"Betty Carter", "price":49.99}'

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sun, 20 Nov 2022 09:54:25 GMT
Content-Length: 116

{
    "id": "4",
    "title": "The Modern Sound of Betty Carter",
    "artist": "Betty Carter",
    "price": 49.99
}

無事に201 Createdとレスポンスボディで追加した情報が返ってきていることが確認できた。
GET /albumsリクエストを送って、追加されたか確認してみる

$ curl http://localhost:8081/albums
[
    {
        "id": "1",
        "title": "Blue Train",
        "artist": "John Coltrane",
        "price": 56.99
    },
    {
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
    },
    {
        "id": "3",
        "title": "Sarah Vaughan and Clifford Brown",
        "artist": "Sarah Vaughan",
        "price": 39.99
    },
    {
        "id": "4",
        "title": "The Modern Sound of Betty Carter",
        "artist": "Betty Carter",
        "price": 49.99
    }
]

問題なく、表示された。

特定のアイテムを返すハンドラーの実装

ロジックを考える

GET /albums/[id]のリクエストを送信したとき、パスパラメータの[id]に一致するIDを持つアルバム情報を返すようにする。

  • 要求されたアルバムを取得するロジックを実装。
  • パスをロジックにマッピングする。

実装

IDに紐づくアルバム情報を返すロジックと、ルーティング処理をmain.goに追加していく。
それ以外の部分は変更がないため、省略。

main.go
//...長いの省略!
func getAlbumById(c *gin.Context) {
	id := c.Param("id")

	for _, a := range albums {
		if a.ID == id {
			c.IndentedJSON(http.StatusOK, a)
			return
		}
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
func main() {
	router := gin.Default()
	router.GET("/albums", getAlbums)
	router.GET("/albums/:id", getAlbumById)
	router.POST("/albums", postAlbums)

	router.Run("localhost:8081")
}

URLのパスパラメータを取得するには、Context.Paramを使用する。
albumスライス内の構造体をループして、ID値が一致する構造体を探す。一致するIDが見つかった場合は、その構造体をJSONにシリアル化して、HTTPコードと共にレスポンスとして返す。
見つからなかった場合は、404エラーとメッセージを返す。
mainに、GET /albums/:idをリクエストしたときにgetAlbumByIdにルーティングする処理を追加する。

コードの実行

一旦サーバを停止し、再度コードを実行する。

$ go run .

curlでID2のアルバムを取得してみる。

$ curl http://localhost:8081/albums/2
{
    "id": "2",
    "title": "Jeru",
    "artist": "Gerry Mulligan",
    "price": 17.99
}

次にID4のアルバムを取得してみる。

$ curl http://localhost:8081/albums/4
{
    "message": "album not found"
}

先ほどPOSTで追加したが、サービスを一度停止させているため、メモリ内のデータがリセットされ、ID4のデータは存在しないので、Not Foundになっている。

まとめ

簡単なWebサービスを作ることができた。
Pythonの時もFlask, PHPではSlimを使ってたけど、この手の軽量Webフレームワークはお手軽に作れるので嬉しい。
実際に使う場合は、データベース連携や、認証機能、セッション管理、パフォーマンスなどを気にしなければならないがチュートリアルではこのぐらいがちょうどいい。

完成したコード

main.go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

var albums = []album{
	{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
	{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
	{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func getAlbums(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, albums)
}

func postAlbums(c *gin.Context) {
	var newAlbum album

	if err := c.BindJSON(&newAlbum); err != nil {
		return
	}

	albums = append(albums, newAlbum)
	c.IndentedJSON(http.StatusCreated, newAlbum)
}

func getAlbumById(c *gin.Context) {
	id := c.Param("id")

	for _, a := range albums {
		if a.ID == id {
			c.IndentedJSON(http.StatusOK, a)
			return
		}
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
func main() {
	router := gin.Default()
	router.GET("/albums", getAlbums)
	router.GET("/albums/:id", getAlbumById)
	router.POST("/albums", postAlbums)

	router.Run("localhost:8081")
}

Discussion