【Go言語入門⑥】GoとGinを使用したRESTful APIの開発

Go言語
目次

過去の記事

下記の内容を実施している前提となりますのでご了承ください

今回の内容

チュートリアルには、次のセクションが含まれています。

  1. API エンドポイントを設計します。
  2. コード用のフォルダーを作成します。
  3. データを作成します。
  4. すべてのアイテムを返すハンドラーを作成します。
  5. 新しいアイテムを追加するハンドラーを作成します。
  6. 特定のアイテムを返すハンドラーを作成します。

前提条件

  • Go 1.16 以降のインストール。インストール手順については、 Go のインストールを参照してください。
  • コードを編集するためのツール。お使いのテキスト エディタはどれでも問題なく動作します。
  • コマンド端末です。Go は、Linux と Mac の任意のターミナル、および Windows の PowerShell または cmd で適切に動作します。
  • カールツール。Linux と Mac では、これは既にインストールされているはずです。Windows では、Windows 10 Insider ビルド 17063 以降に含まれています。以前の Windows バージョンでは、インストールが必要になる場合があります。詳細については、「 Tar と Curl が Windows に登場」を参照してください。

API エンドポイントの設計

/albums

  • GET – 全アルバムの一覧を取得し、JSONで返す。
  • POST – JSONで送られたリクエストデータから、新しいアルバムを追加する。

/albums/:id

  • GET – IDでアルバムを取得し、アルバムデータをJSONで返す。

コード用のフォルダーを作成する

STEP
ディレクトリを作成
$ mkdir web-service-gin
$ cd web-service-gin
STEP
依存関係を管理できるモジュールを作成
$ go mod init example/web-service-gin
go: creating new go.mod: module example/web-service-gin

データを作成する

チュートリアルを簡単にするために、データをメモリに保存します。より典型的な API は、データベースと対話します。

データをメモリに保存すると、サーバーを停止するたびにアルバムのセットが失われ、サーバーを起動すると再作成されることに注意してください。

STEP
web-service ディレクトリに main.go というファイルを作成
STEP
main.goに下記を記述
package main

// album はレコードのアルバムに関するデータを表す。
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},
}

すべてのアイテムを返すハンドラーを作成する

クライアントが でリクエストを行ったときに、GET /albumsすべてのアルバムを JSON として返す必要があります。

これを行うには、次のように記述します。

  • 応答を準備するロジック
  • リクエスト パスをロジックにマップするコード

これは実行時に実行される方法の逆ですが、最初に依存関係を追加し、次にそれらに依存するコードを追加していることに注意してください。

STEP
getAlbums()を定義
package main

// album はレコードのアルバムに関するデータを表す。
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},
}

// getAlbumsは、全アルバムのリストをJSONで応答します
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

このコードでは、次のことを行います。

  • gin.Contextパラメータを受け取るgetAlbums関数を作成します。GinもGoも特定の関数名形式を要求していませんので、この関数には任意の名前を付けることができます。

    gin.Contextは、Ginの最も重要な部分です。リクエストの詳細、JSONの検証やシリアライズなどを行います。(似たような名前ですが、これはGoの組み込みコンテキストパッケージとは異なります)。
  • Context.IndentedJSON を呼び出して、構造体を JSON にシリアライズし、レスポンスに追加します。
    この関数の最初の引数は、クライアントに送信したいHTTPステータスコードです。ここでは、200 OK を示すために、net/http パッケージの StatusOK 定数を渡しています。

    なお、Context.IndentedJSON Context.JSON の呼び出しに置き換えることで、よりコンパクトな JSON を送信することができます。実際には、インデントされた形の方がデバッグ時に作業しやすく、サイズの差も通常は小さくなります。
STEP
main()を追加
package main

// album はレコードのアルバムに関するデータを表す。
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 main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)

    router.Run("localhost:8080")
}

// getAlbumsは、全アルバムのリストをJSONで応答します
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

このコードでは次のことをしています

  • DefaultでGinルータを初期化する。
  • GET HTTPメソッドと/albumsパスをハンドラ関数に関連付けるには、GET関数を使用します。

    getAlbums 関数の名前を渡していることに注意してください。これは、getAlbums() を使って関数の結果を渡すのとは違います(括弧に注目してください)。
  • Run機能を使って、ルーターをhttp.Serverにアタッチし、サーバーを起動します。
STEP
必要なパッケージをインポートする
package main

import (
    "net/http"

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


// album はレコードのアルバムに関するデータを表す。
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 main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)

    router.Run("localhost:8080")
}

// getAlbumsは、全アルバムのリストをJSONで応答します
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

main.goを保存します

そして、モジュールを取り込んで、go.modを更新します。

$ go mod tidy
go: finding module for package github.com/gin-gonic/gin
go: downloading github.com/gin-gonic/gin v1.8.1
go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.8.1
go: downloading github.com/gin-contrib/sse v0.1.0
go: downloading github.com/mattn/go-isatty v0.0.14

すると、go.modが次のように更新されます

module example/web-service-gin

go 1.19

require github.com/gin-gonic/gin v1.8.1

require (
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/go-playground/locales v0.14.0 // indirect
	github.com/go-playground/universal-translator v0.18.0 // indirect
	github.com/go-playground/validator/v10 v10.10.0 // indirect
	github.com/goccy/go-json v0.9.7 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/leodido/go-urn v1.2.1 // indirect
	github.com/mattn/go-isatty v0.0.14 // indirect
	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.0.1 // indirect
	github.com/ugorji/go/codec v1.2.7 // indirect
	golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
	golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
	golang.org/x/text v0.3.6 // indirect
	google.golang.org/protobuf v1.28.0 // indirect
	gopkg.in/yaml.v2 v2.4.0 // indirect
)

コードを実行する

下記のコードの箇所でルーティングを記述しました。

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

    router.Run("localhost:8080")
}

こちらの動作確認をしたいと思います。

まずは起動前に実行します。下記の表示されたら問題ありません。

$curl http://localhost:8080/albums
curl: (7) Failed to connect to localhost port 8080: Connection refused

localhost:8080は未使用なので、当然繋がりません。

逆に下記が表示されたら注意が必要です

$curl http://localhost:8080/albums
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 404 Not Found</title>
</head>
<body><h2>HTTP ERROR 404 Not Found</h2>
<table>
<tr><th>URI:</th><td>/albums</td></tr>
<tr><th>STATUS:</th><td>404</td></tr>
<tr><th>MESSAGE:</th><td>Not Found</td></tr>
<tr><th>SERVLET:</th><td>default</td></tr>
</table>
<hr/><a href="https://eclipse.org/jetty">Powered by Jetty:// 11.0.7</a><hr/>

</body>
</html>

この場合、ポート番号8080は使用中です。Dockerを閉じたり、8080ポートをkillしてください。

問題がないことを確認したところで、起動します。go run .でmain()を実行します

$ go run .
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /albums                   --> main.getAlbums (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on localhost:8080

無事に起動できたら、下記を実行します

$curl http://localhost:8080/albums

すると、先ほど定義した配列がreturnされます。

$curl http://localhost:8080/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
    }
]%

これで一覧取得処理は完了です

Create処理を実装する

クライアントから にリクエストがあった場合POST/albumsリクエストボディに記載されたアルバムを既存のアルバムのデータに追加したい。

これを行うには、次のように記述します。

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

コードを書く

STEP
アルバム データをアルバムのリストに追加するコードを追加します。

次のコードを追加します

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

    // Call BindJSON to bind the received JSON to
    // newAlbum.
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Add the new album to the slice.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

このコードでは、次のことを行います。

  • Context.BindJSON リクエスト本文を にバインドするために使用しnewAlbumます。
  • albumJSON から初期化された構造体をalbums スライスに追加します。
  • 追加し201たアルバムを表す JSON と共に、ステータス コードを応答に追加します。
STEP
main()にルーティングを追加する
func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.POST("/albums", postAlbums) // 追加

    router.Run("localhost:8080")
}
STEP
再起動する
$ go run .

上記実施しないと、Not Foundになります

curl http://localhost:8080/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 404 Not Found
Content-Type: text/plain
Date: Tue, 04 Oct 2022 07:23:40 GMT
Content-Length: 18
STEP
実行する
$curl http://localhost:8080/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: Tue, 04 Oct 2022 07:24:02 GMT
Content-Length: 116

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

追加されたらか確認します

$ curl http://localhost:8080/albums \
    --header "Content-Type: application/json" \
    --request "GET"

すると無事に表示されました

[
    {
        "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 を持つアルバムを返したいとします。

これを行うには、次のことを行います。

  • 要求されたアルバムを取得するロジックを追加します。
  • パスをロジックにマップします。

コードを書く

STEP
getAlbumByID()を追加

このgetAlbumByID関数は、リクエスト パスの ID を抽出し、一致するアルバムを見つけます。

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

    // Loop over the list of albums, looking for
    // an album whose ID value matches the parameter.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

このコードでは、次のことを行います。

  • URL からパス パラメーターContext.Param を取得するために使用します。idこのハンドラーをパスにマップするときは、パラメーターのプレースホルダーをパスに含めます。
  • albumスライス内の構造体をループして、フィールド値がパラメーター値とID 一致する構造体を探します。id見つかった場合は、その構造体を JSON にシリアル化し、 HTTP コードalbumを含む応答として返します。200 OK前述のように、実際のサービスでは、データベース クエリを使用してこのルックアップを実行する可能性があります。
  • アルバムが見つからない場合はHTTP404エラーを返します。http.StatusNotFound
STEP
ルーティングを追加
func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)  // 追加
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

このコードでは、次のことを行います。

  • /albums/:idパスをgetAlbumByID関数に関連付けます。Gin では、パス内の項目の前にあるコロンは、その項目がパス パラメーターであることを示します。
STEP
コマンドを実行

まずはアプリを停止します。

その後に再度起動します

$ go run .

起動ができたら、下記を実行します。

$ curl http://localhost:8080/albums/2  

すると、値を取得できました

{
    "id": "2",
    "title": "Jeru",
    "artist": "Gerry Mulligan",
    "price": 17.99
}%

これで実装は以上です

次に取り組む

おめでとう!Go と Gin を使用して、単純な RESTful Web サービスを作成しました。

次のチュートリアルに進んでいきましょう!

ぎゅう
WEBエンジニア
渋谷でWEBエンジニアとして働く。
LaravelとVue.jsをよく取り扱い、誰でも仕様が伝わるコードを書くことを得意とする。
先輩だろうがプルリクにコメントをして、リファクタしまくる仕様伝わるコード書くマン
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次
閉じる