lelelemon’s blog

カメの歩みでのんびり学んでいます。

【Go言語】go/gin で簡単なREST API を作成

Go言語で gin フレームワークを使って REST API を作成する手順について記載しています。

 

実行環境について

下記の環境で動かしています。

  • ubunth "20.04.4 LTS (Focal Fossa) <- WSL2
  • go version go1.21.6 linux/amd64
  • docker: 20.10.17

 

DB環境を準備

今回は PostgreSQL を使用しています。

docker-compose.yml
version: '3'
services:
  postgres:
    image: 'postgres:14'
    container_name: postgres_gin_rest
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: sample
      TZ: Asia/Tokyo
      PGDATA: /var/lib/postgresql/data/pgdata
    ports:
      - '5432:5432'
    volumes:
      - './db-store:/var/lib/postgresql/data'

 

テーブルを作成

docker exec -it postgres_gin_rest psql -U user -d sample

上記で PostgreSQL に接続し、下記のテーブルCREATE文を実行します。

 

CREATE TABLE "user" (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255),
    age INT
);

 

ここで作成したテーブルに対して CRUD を行う REST API を作成していきます。

 

gin を導入

まずは gin を使えるようにします。

go mod init {プロジェクト名}

で go.mod ファイルを作成。

 

main.go を作成し、中身を記載します。

main.go
package main

import (
    "net/http"

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

func main() {
    r := gin.Default()
    r.GET("/", handleHome)
    r.Run()
}

func handleHome(ctx *gin.Context) {
    ctx.JSON(http.StatusOK, gin.H{"message": "test"})
}

 

下記の import でコンパイルエラーになるので、

github.com/gin-gonic/gin

 

プロジェクトルートで「go mod tidy」コマンドを実行し、gin の依存関係を追加します。

 

go run main.go 
[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    /                         --> main.handleHome (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] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

 

main.go を実行すると 8080 ポートで起動するので、「http://localhost:8080」にブラウザからアクセスしてみます。

{
    "message": "test"
}

上記が画面に表示されれば無事に接続できています。

 

gin で REST API 構築ー登録処理

続いて、事前に作成した BOOK テーブルに対してデータ登録のエンドポイントを作成していきます。

このエンドポイントは、下記の仕様とします。

  • エンドポイントは http://localhost:8080/user
  • POSTメソッドを受け付け、リクエストボディーで指定された値をもとにUserテーブルにデータを登録する
  • 登録が完了したら、HTTPステータス:200と、登録されたUserレコードの値をレスポンスする
  • リクエストボディーに不備がある場合は、HTTP ステータス:400と、エラー情報をレスポンスする
  • その他想定外のエラーが発生した場合は、HTTP ステータス:500と、エラー情報をレスポンスする

 

まずはBOOKテーブルに該当するモデルクラスを作成します。(model パッケージを作成してその中に model.go ファイルを作成しています)

 

model/model.go
package model

type User struct {
    Id   int
    Name string
    Age  int
}

 

 

続いて、データ登録処理を書きます。(こちらも、funcs パッケージを作成してその中に funcs.go ファイルを作成しています)

 

funcs/funcs.go
package funcs

import (
    "database/sql"
    "fmt"
    "gin-rest/model"
    "log"
)

func InsertUser(db *sql.DB, user model.User) (model.User, error) {
    var insertedUser model.User

    err := db.QueryRow(
        "INSERT INTO public.user (name, age)"+
            "VALUES ($1, $2) RETURNING id, name, age",
        user.Name, user.Age).Scan(
        &insertedUser.Id, &insertedUser.Name, &insertedUser.Age,
    )

    if err != nil {
        log.Fatal("error inserting user")
        fmt.Println(err)
        return model.User{}, err
    }

    return insertedUser, nil
}

 

main.go には下記を追記します。

main.go
func handleCreateUser(ctx *gin.Context) {
    var user model.User

    // リクエストボディからデータを取得
    if err := ctx.ShouldBindJSON(&user); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user, err := funcs.InsertUser(DB, user)
    if err != nil {
        log.Fatal(err)
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    ctx.JSON(http.StatusOK, gin.H{"user": user})
}
 

 

package main

import (
    "database/sql"
    "gin-rest/funcs"
    "gin-rest/model"
    "log"
    "net/http"

    "github.com/gin-gonic/gin"

    _ "github.com/lib/pq"
)

var DB *sql.DB

func main() {
    initDB()
    r := gin.Default()
    // r.Use(corsMiddleware())
    r.GET("/", handleHome)
    r.POST("/user", handleCreateUser)
    r.Run()
}

 

POST リクエストを受け付け、リクエストボディーから USER テーブルの登録内容を読み取り、データ登録するようにしました。

登録が完了したら、HTTPレスポンスステータス:200と、登録されたデータをレスポンスボディーに含めて返すようにしています。

 

また、下記のimportがコンパイルエラーになるため、

github.com/lib/pq

 

go mod tidy

pq の依存を追加します。

 

それでは、main.go を起動し、POSTリクエストを送信してみます。

 

curl -X POST -H "Content-Type: application/json" -d '{"Name": "User1", "Age": 40}' http://localhost:8080/user
{"user":{"Id":1,"Name":"User1","Age":40}}

 

レスポンスが返ってきたので、PostgreSQL にもつないでデータ登録されているか確認します。

 

sample=# select * from public.user;
 id | name  | age 
----+-------+-----
  1 | User1 |  40
(1 row)

無事に登録できています。

 

gin で REST API 構築ー検索処理

続いて、データ検索のエンドポイントを作成していきます。

このエンドポイントは、下記の仕様とします。

  • エンドポイントは http://localhost:8080/user/{検索したいユーザーID}
  • GETメソッドを受け付け、パスパラメーターで指定されたUserIDをもとにUserテーブルを検索する
  • データが取得ができたら、HTTPステータス:200と、取得されたUserレコードの値をレスポンスする
  • リクエストに不備がある場合は、HTTP ステータス:400と、エラー情報をレスポンスする
  • その他想定外のエラーが発生した場合は、HTTP ステータス:500と、エラー情報をレスポンスする

 

DB 検索処理として、下記を追記します。

funcs/funcs.go
func GetUserByID(db *sql.DB, id int) (model.User, error) {
    row := db.QueryRow("SELECT id, name, age FROM public.user WHERE id = $1", id)

    var user model.User
    err := row.Scan(&user.Id, &user.Name, &user.Age)
    if err != nil {
        log.Fatal("error scanning user")
        return model.User{}, err
    }

    return user, nil
}

 

検索のリクエストをハンドリングできるようにするため、main.go には下記を追記します。

main.go
func handleGetUserByID(ctx *gin.Context) {
    // パスパラメーター "id" を取得
    id := ctx.Param("id")

    // id を整数に変換
    userID, err := strconv.Atoi(id)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }

    // GetBookByID 関数を呼び出し
    user, err := funcs.GetUserByID(DB, userID)
    if err != nil {
        log.Fatal(err)
        ctx.JSON(http.StatusInternalServerError,
            gin.H{"error": "Failed to retrieve user"})
        return
    }

    ctx.JSON(http.StatusOK, gin.H{"user": user})
}

 

package main

import (
    "database/sql"
    "gin-rest/funcs"
    "gin-rest/model"
    "log"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"

    _ "github.com/lib/pq"
)

var DB *sql.DB

func main() {
    initDB()
    r := gin.Default()
    r.GET("/", handleHome)
    r.POST("/user", handleCreateUser)
    r.GET("/user/:id", handleGetUserByID)
    r.Run()
}

 

  • ctx.Param でパスパラメーターから値を取得することができます。これは r.GET("/user/:id" の定義箇所になります。
  • strconv.Atio で文字列を数値にパースします。

 

main.go を起動して、http://localhost:8080/user/1 にブラウザからアクセスしてみます。

以下の内容がブラウザ上で確認できると思います。

 

 

 

gin で REST API 構築ー更新処理

続いて、データ更新のエンドポイントを作成していきます。

このエンドポイントは、下記の仕様とします。

  • エンドポイントは http://localhost:8080/user/{更新したいユーザーID}
  • PUTメソッドを受け付け、リクエストボディーで指定された値をもとにUserテーブルの指定IDのデータを更新する
  • 更新が完了したら、HTTPステータス:200と、更新されたUserレコードの値をレスポンスする
  • リクエストボディーに不備がある場合は、HTTP ステータス:400と、エラー情報をレスポンスする
  • その他想定外のエラーが発生した場合は、HTTP ステータス:500と、エラー情報をレスポンスする

DB 更新処理として、下記を追記します。

funcs/funcs.go
func UpdateUser(db *sql.DB, user model.User) (model.User, error) {
    var updatedUser model.User

    err := db.QueryRow(
        "UPDATE public.user SET name=$1, age=$2 WHERE id=$3"+
            " RETURNING id, name, age",
        user.Name, user.Age, user.Id,
    ).Scan(&updatedUser.Id, &updatedUser.Name, &updatedUser.Age)

    if err != nil {
        log.Fatal("error updating user")
        return model.User{}, err
    }

    return updatedUser, nil
}

 

更新のリクエストをハンドリングできるようにするため、main.go には下記を追記します。

main.go
func handleUpdateUser(ctx *gin.Context) {
    // パスパラメーター "id" を取得
    id := ctx.Param("id")

    // id を整数に変換
    userID, err := strconv.Atoi(id)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }

    var user model.User

    // リクエストボディからデータを取得
    if err := ctx.ShouldBindJSON(&user); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // リクエストボディーに含まれるIDを更新対象のIDとして設定
    user.Id = userID

    user, err = funcs.UpdateUser(DB, user)
    if err != nil {
        log.Fatal(err)
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    ctx.JSON(http.StatusOK, gin.H{"user": user})
}

 

func main() {
    initDB()
    r := gin.Default()
    r.GET("/", handleHome)
    r.POST("/user", handleCreateUser)
    r.GET("/user/:id", handleGetUserByID)
    r.PUT("/user/:id", handleUpdateUser)
    r.Run()
}

 

  • ctx.Param でパスパラメーターから更新対象のユーザーIDを取得しています
  • リクエストボディーを読み込み、更新対象のユーザーモデルにマッピングします

 

main.go を起動し、PUTリクエストを送信してみます。

curl -X PUT -H "Content-Type: application/json" -d '{"Name": "Updated User1", "Age": 30}' http://localhost:8080/user/1
{"user":{"Id":1,"Name":"Updated User1","Age":30}}

レスポンスが返ってきたので、PostgreSQL にもつないでデータ登録されているか確認します。

 

select * from public.user;
 id |     name      | age 
----+---------------+-----
  1 | Updated User1 |  30
(1 row)

無事に更新できました。

 

gin で REST API 構築ー削除処理

最後に、データ削除のエンドポイントを作成していきます。

このエンドポイントは、下記の仕様とします。

  • エンドポイントは http://localhost:8080/user/{削除したいユーザーID}
  • DELETEメソッドを受け付け、パスパラメーターで指定されたUserIDに該当するユーザーを削除する
  • データが削除ができたら、HTTPステータス:200をレスポンスする
  • リクエストに不備がある場合は、HTTP ステータス:400と、エラー情報をレスポンスする
  • その他想定外のエラーが発生した場合は、HTTP ステータス:500と、エラー情報をレスポンスする

 

DB 削除処理として、下記を追記します。

funcs/funcs.go
func DeleteUser(db *sql.DB, id int) error {
    existingUser, err := GetUserByID(db, id)
    if err != nil {
        log.Fatal("error deleting user")
        return err
    }

    // ユーザーが存在しない場合はエラーを返す
    if existingUser.Id == 0 {
        return errors.New("user not found")
    }

    // ユーザーが存在する場合は削除を実行
    _, err = db.Exec("DELETE FROM public.user WHERE id = $1", id)
    if err != nil {
        log.Fatal("error deleting user")
        return err
    }

    return nil
}

 

  • 指定IDのユーザーが存在するかDBに問い合わせ、存在する場合に削除処理を実行します

 

削除のリクエストをハンドリングできるようにするため、main.go には下記を追記します。

main.go
func handleDeleteUser(ctx *gin.Context) {
    // パスパラメーター "id" を取得
    id := ctx.Param("id")

    // id を整数に変換
    userID, err := strconv.Atoi(id)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }

    err = funcs.DeleteUser(DB, userID)
    if err != nil {
        log.Fatal(err)
        ctx.JSON(http.StatusInternalServerError,
            gin.H{"error": err.Error()})
        return
    }

    ctx.JSON(http.StatusOK, gin.H{})
}

 

func main() {
    initDB()
    r := gin.Default()
    r.GET("/", handleHome)
    r.POST("/user", handleCreateUser)
    r.GET("/user/:id", handleGetUserByID)
    r.PUT("/user/:id", handleUpdateUser)
    r.DELETE("/user/:id", handleDeleteUser)
    r.Run()
}

 

  • ctx.Param でパスパラメーターから削除対象のユーザーIDを取得し、削除処理を実行します

 

main.go を起動し、DELETEリクエストを送信してみます。

 

curl -X DELETE http://localhost:8080/user/1
{}

 

PostgreSQL にもつないでデータ削除されているか確認します。

 

select * from public.user;
 id | name | age 
----+------+-----
(0 rows)

 

無事に削除されました。

 

以上、簡単ですがgo/ginを使ったREST API作成のサンプルです。

gin フレームワークを使用することでシンプルなコードでAPI開発が楽に実装できるのはもちろん、今回は書けていませんがHTMLを返したりもできるので画面開発もできたりします。

似たようなフレームワークEcho もあるので、このあたりも学んでいきたい。