Go + ginを使って簡単なAPIを作る

Goのフレームワークである、ginを使って簡易的なAPIを作成していきたいと思います。

ginとは

ginは、Goで書かれたWebアプリケーションフレームワークになります。 高速なパフォーマンス、ミドルウェアが充実、JSONのリクエストのバリデーション、ルーティングのグループ化、エラー管理、組み込みのレンダリング、拡張性があるなどの特徴を持ちます。

インストール

$ go get -u github.com/gin-gonic/gin

コード内でインポートする

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

これで、ginを使用することができます、簡単ですね!

docker環境作成

Docker環境で、行いたいと思うので、以下ファイルを作成します。説明については割愛させていただきます。 ホットリロードするために、Airを導入しています。

FROM golang:latest

WORKDIR /go/src

COPY ./ .

RUN go install github.com/cosmtrek/air@latest

CMD ["air", "-c", ".air.toml"]
version: '3'
services:
  go:
    build:
      context: .
    volumes:
      - ./:/go/src
    ports:
      - "8080:8080"
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary, can setup environment variables when run your app.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = ["hello", "world"]

[log]
# Show log time
time = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

API作成

それでは、APIの作成を行なっていきます。

package main

import (
    "errors"
    "net/http"

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

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

var books = []book{
    {ID: "1", Title: "In Search of Lost Time", Author: "Marcel Proust", Quantity: 2},
    {ID: "2", Title: "The Great Gatsby", Author: "F. Scott Fitzgerald", Quantity: 5},
    {ID: "3", Title: "War and Peace", Author: "Leo Tolstoy", Quantity: 6},
}

func getBooks(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, books)
}

func main() {
    router := gin.Default()

    router.GET("/books", getBooks)

    router.Run()
}

gin.Default

gin.Defaultは、*gin.Engine構造体を返します。ginでは、このEngine構造体を使って,エンドポイント、ミドルウェアなどを登録しておくことができます。

gin.Context

gin.Contextは、リクエストに関連する情報が格納されており、また応答を返すことができます。 クエリパラメータ、データ、ペイロード、ヘッダーなどが格納されていて、アクセスすることが可能です。 標準のGoで、HandleFuncを作成する際に、引数にResponseWriter*http.Requestを渡すと思いますが、Context構造体のフィールドにも存在します。 Run()メソッドも、内部では、http.ListenAndServe()を呼び出してます。 標準のGoで、サーバー立ち上げがわかる方なら、置き換わってると考えれば理解しやすいと思います。

コードは、localhost:8080/booksにアクセスすると、getBooksが呼ばれます。処理としては、book構造体のスライスであるbooksをJSONとしてシリアライズして、返しています。 動かしてみます

$ docker-compose up
go_1  | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
go_1  | [GIN-debug] Listening and serving HTTP on :8080

立ち上がったので、curlコマンドを使用し、作成したエンドポイントにカールします。

$ curl localhost:8080/books
[
    {
        "id": "1",
        "title": "In Search of Lost Time",
        "author": "Marcel Proust",
        "quantity": 2
    },
    {
        "id": "2",
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "quantity": 5
    },
    {
        "id": "3",
        "title": "War and Peace",
        "author": "Leo Tolstoy",
        "quantity": 6
    }
]         

インデントされたJSONが作成され返されてますね! 次は、bookに新しく、追加できるようにしていきたいと思います。

func createBook(c *gin.Context) {
    var newBook book

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

    books = append(books, newBook)
    c.IndentedJSON(http.StatusCreated, newBook)
}

func main() {
    router := gin.Default()

    router.GET("/books", getBooks)
    router.POST("/books", createBook)

    router.Run()
}

追加するための、JSONファイルを作成します。

{
  "id": "4",
  "title": "Hamlet",
  "author": "William Shakespeare",
  "quantity": 2
}

コンテナを立ち上げます。

docker-compose up
go_1  | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
go_1  | [GIN-debug] Listening and serving HTTP on :8080

立ち上がったので、先ほど作成したJSONファイルを送信してみたと思います。

$ curl localhost:8080/books --include --header "Content-Type: application/json" -d @body.json --request "POST"

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sat, 24 Sep 2022 06:16:13 GMT
Content-Length: 96

{
    "id": "4",
    "title": "Hamlet",
    "author": "William Shakespeare",
    "quantity": 2
}               

追加することができたと思います。 コードは、リクエストからきたJSONを、book構造体にバインドして、booksの中にappendして、インデントされたJSONを返します。 次は、特定の本を取得してみたいと思います。

func bookById(c *gin.Context) {
    id := c.Param("id")
    book, err := getBookById(id)
    if err != nil {
        c.IndentedJSON(http.StatusNotFound, gin.H{"message": "Book not found."})
        return
    }
    c.IndentedJSON(http.StatusOK, book)
}

func getBookById(id string) (*book, error) {
    for i, b := range books {
        if b.ID == id {
            return &books[i], nil
        }
    }

    return nil, errors.New("book not found")
}

func main() {
    router := gin.Default()

    router.GET("/books", getBooks)
    router.GET("/books/:id", bookById)
    router.POST("/books", createBook)

    router.Run()
}

コンテナを立ち上げます。

$ docker-compose up
go_1  | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
go_1  | [GIN-debug] Listening and serving HTTP on :8080

立ち上がったので、特定の本を取得してみたいと思います。

$ curl localhost:8080/books/2
{
    "id": "2",
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald",
    "quantity": 5
}          

指定した本を取得することができました。本が存在しない場合も試してみます。存在しないidを指定してみます。

$ curl localhost:8080/books/5
{
    "message": "Book not found."
}

"message": "Book not found."存在しないid指定した時の動作も問題無さそうですね。

以上となります。 Goは、標準のライブラリが充実してるので、そちらを使用するのも良いと思いますが、規模が大きくなるとginなどのフレームワークを使用した方が効率よく開発出来ると思いますので、触ってみてはいかがでしょうか。