GoのフレームワークGinを利用してRESTfulAPIを開発する方法

Golang_Gin

こんにちはMinatoです。

今回は人気のWebフレームワークGinを利用して、RESTfulAPIを開発する方法を解説します。

Ginとは

ginはGo(Golang)で書かれたWebフレームワークです。 httprouterのおかげで、最大40倍高速なパフォーマンスを備えたmartiniのようなAPIを備えています。パフォーマンスと優れた生産性が必要な場合は、Ginを好きになるでしょう。

https://gin-gonic.com/ja/docs/introduction/

Ginの人気について

GinはGoのWebフレームワークの中でも、特に人気があります。以下はGithubのStarの数を示しています。

Project NameStarsForksOpen IssuesDescriptionLast Commit
gin518945889443Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance — up to 40 times faster. If you need smashing performance, get yourself some Gin.2021-09-30 02:04:28
beego27032531627beego is an open-source, high-performance web framework for the Go programming language.2021-09-18 15:08:26
kit21360219247A standard library for microservices.2021-09-28 15:01:29
echo20797184165High performance, minimalist Go web framework2021-09-26 15:56:43
https://github.com/mingrammer/go-web-framework-stars

概要

今回は簡単なTODOアプリを想定して、TODOを操作するAPIの開発を行います。

話を簡略化するために今回はデータをメモリ上に保存します。通常のアプリケーション開発ではデータはデータベースなどで管理することになります。

エンドポイント

メソッドエンドポイント説明
GET/todosTODOの一覧を取得します
POST/todos新規にTODOを作成します
GET/todos/:id指定されたTODO1件を取得します
PATCH/todos/:id指定されたTODOを更新します
DELETE/todos/:id指定されたTODOを削除します

実装

コード用のディレクトリの作成

まずはコードを格納するディレクトリを作成します。

mkdir todo-api
cd todo-api

依存関係を管理できるモジュールを作成

go mod init example/todo-api

ライブラリのインストール

go get github.com/gin-gonic/gin

Structとデータの作成

まずは、現在のディレクトリにmain.goを作成し、以下の内容を記述します。

まずStruct todoを作成します。todoはIDとTitle、Authorのフィールドを持ちます。

json:"id" の箇所ではStructをJsonに変換した際のフィールド名を明示的に指定しています。

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
  "strconv"
)

type todo struct {
	ID     int  `json:"id"`
	Title  string  `json:"title"`
	Author string  `json:"author"`
}

var todos = []todo{
	{ID: 1, Title: "タイトルA", Author: "Taro"},
	{ID: 2, Title: "タイトルB", Author: "Jiro"},
	{ID: 3, Title: "タイトルC", Author: "Takuya"},
}

func main() {

}

全てのtodo返すハンドラの作成

main()の上に以下の関数を定義します。

以下のコードでは、todo StructのスライスからJSONを作成し、JSONをレスポンスに書き込んでいます。

func getTodos(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, todos)
}

次にmain()に以下のコードを追加します。

gin.Default()で デフォルトルーターを初期化します。

router.GET("/todos", getTodos)では、GETメソッドでの/todosへのリクエストとgetTodos()ハンドラを紐づけています。

router.Run("localhost:8080")でrouterをhttp.Serverに紐付け、サーバーを起動しています。

func main() {
	router := gin.Default()
	router.GET("/todos", getTodos)

	router.Run("localhost:8080")
}

実装が完了したので、一度プログラムを実行して、動作の確認を行います。

http://localhost:8080/todos へGETでアクセスすると返却されます。

# 実行
go run .
curl http://localhost:8080/todos
[
    {
        "id": 1,
        "title": "タイトルA",
        "author": "Taro"
    },
    {
        "id": 2,
        "title": "タイトルB",
        "author": "Jiro"
    },
    {
        "id": 3,
        "title": "タイトルC",
        "author": "Takuya"
    }
]

TODOを新規作成するハンドラの作成

main()の上に以下の関数を定義します。

c.BindJSON(&newTodo) でリクエストボディをnewTodoにバインドしています。

append(todos, newTodo)でtodosスライスに作成したnewTodoを追加しています。

最後に、newTodoをJSONに変換し、レスポンスに書き込んでいます。

func postTodo(c *gin.Context) {
	var newTodo todo

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

	todos = append(todos, newTodo)
	c.IndentedJSON(http.StatusCreated, newTodo)
}

main()を以下のように変更します。

router.POST("/todos", postTodo)では、POSTメソッドでの/todosへのリクエストとpostTodoハンドラを紐づけています。

func main() {
	router := gin.Default()
	router.GET("/todos", getTodos)
	router.POST("/todos", postTodo)

	router.Run("localhost:8080")
}

サーバーを再起動して、動作を確認します。

POSTメソッドでhttp://localhost:8080/todosへアクセスしてください。

curl http://localhost:8080/todos \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id": 4,"title": "タイトルD","author": "Goro"}'

以下のレスポンスが返却されます。

{
    "id": 4,
    "title": "タイトルD",
    "author": "Goro"
}   

もう一度todoの一覧を取得すると先ほどPOSTしたデータが登録されています。

curl http://localhost:8080/todos
[
    {
        "id": 1,
        "title": "タイトルA",
        "author": "Taro"
    },
    {
        "id": 2,
        "title": "タイトルB",
        "author": "Jiro"
    },
    {
        "id": 3,
        "title": "タイトルC",
        "author": "Takuya"
    },
    {
        "id": 4,
        "title": "タイトルD",
        "author": "Goro"
    }
]

指定したTODO1件を取得するハンドラ

main()の上に以下の関数を定義します。

まずはc.Param("id") を利用して、URLからidを取得します。また、strconv.Atoi()を利用して、idを文字列から数値に変換しています。

for文でtodosから指定したIDのデータを検索し、存在すれば該当のtodoをJSON形式で返却します。

func getTodoById(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return
	}

	for _, t := range todos {
		if t.ID == id {
			c.IndentedJSON(http.StatusOK, t)
			return
		}
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

main()を以下のように変更します。

router.GET("/todos/:id", getTodoById)では、GETメソッドでの/todos/:idへのリクエストとgetTodoByIdハンドラを紐づけています。

サーバーを再起動して、動作を確認します。

GETメソッドでhttp://localhost:8080/todos/:idへアクセスしてください。指定したIDのtodo1件が表示されます。

func main() {
	router := gin.Default()
	router.GET("/todos", getTodos)
	router.GET("/todos/:id", getTodoById)
	router.POST("/todos", postTodo)

	router.Run("localhost:8080")
}

TODOを更新するハンドラ

main()の上に以下の関数を定義します。

まずはc.Param("id") を利用して、URLからidを取得します。また、strconv.Atoi()を利用して、idを文字列から数値に変換しています。

次にc.BindJSON(&todo) でtodoにJSONのリクエストボディをバインドしています。

そして、todosスライスの中の指定されたIDのデータをtodoで置き換えた後にtodoをJSONに変換して返しています。

func patchTodo(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return
	}

	var todo todo
	todo.ID = id
	if err = c.BindJSON(&todo); err != nil {
		return
	}

	for i, t := range todos {
		if t.ID == id {
			todos[i] = todo
			c.IndentedJSON(http.StatusOK, todo)
			return
		}
	}

	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "todo not found"})
}

main()を以下のように変更します。

router.PATCH("/todos/:id", patchTodo)では、PATCHメソッドでの/todos/:idへのリクエストとpatchTodoハンドラを紐づけています。

func main() {
	router := gin.Default()
	router.GET("/todos", getTodos)
	router.GET("/todos/:id", getTodoById)
	router.POST("/todos", postTodo)
	router.PATCH("/todos/:id", patchTodo)

	router.Run("localhost:8080")
}

サーバーを再起動して、動作を確認します。

PATCHメソッドでhttp://localhost:8080/todos/:idへアクセスしてください。

curl http://localhost:8080/todos/2 \
    --include \
    --header "Content-Type: application/json" \
    --request "PATCH" \
    --data '{"title": "XXX","author": "YYY"}'

以下のレスポンスが返却されます。

{
    "id": 2,
    "title": "XXX",
    "author": "YYY"
}

もう一度todoの一覧を取得するとデータが更新されていることを確認できます。

TODOを削除するハンドラ

main()の上に以下の関数を定義します。

まずはc.Param("id") を利用して、URLからidを取得します。また、strconv.Atoi()を利用して、idを文字列から数値に変換しています。

todos = append(todos[:i], todos[i+1:]...) で指定されたIDのtodoをtodosから削除してます。

func deleteTodo(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return
	}

	for i, t := range todos {
		if t.ID == id {
			todos = append(todos[:i], todos[i+1:]...)
			c.IndentedJSON(http.StatusOK, gin.H{"message": "todo(" + strconv.Itoa(id) + ") is deleted"})
			return
		}
	}

	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "todo not found"})
}

main()を以下のように変更します。

router.DELETE("/todos/:id", deleteTodo)では、DELETEメソッドでの/todos/:idへのリクエストとdeleteTodoハンドラを紐づけています。

func main() {
	router := gin.Default()
	router.GET("/todos", getTodos)
	router.GET("/todos/:id", getTodoById)
	router.POST("/todos", postTodo)
	router.PATCH("/todos/:id", patchTodo)
	router.DELETE("/todos/:id", deleteTodo)

	router.Run("localhost:8080")
}

サーバーを再起動して、動作を確認します。

DELETEメソッドでhttp://localhost:8080/todos/:idへアクセスしてください。

指定したIDのtodoがtodosから削除されます。

curl http://localhost:8080/todos/2 \
    --request "DELETE"

まとめ

以上でRESTfulAPIの実装は完了です。

Ginを利用することで簡単にRESTfulAPIを開発することができます。

本記事を読んでGinに興味を持たれた方は公式のドキュメントを確認してください。

https://gin-gonic.com/ja/

最後まで読んでいただき、ありがとうございます。

この記事が、「面白いな」、「勉強になったな」という方は、SNSでシェアしていただけると嬉しいです。