【Go言語】EchoとGORMでREST API(CRUD)を実装する

Go言語

Go言語によるREST APIの実装方法を紹介します。
今回は例としてシンプルなCRUD API(GETPOSTPUTDELETE)を実装します。

Go言語は1.16.3、フレームワークはEcho v4.2.2、O/RマッパはGORM v1.21.9、データベースはMySQL 8.0.21を利用します。

以下のような手順でREST APIを実装します。

今回の実装手順
  1. Docker環境の構築
  2. EchoでHTTPサーバを実装する
  3. GORMでデータベースにアクセスする
  4. 『Echo + GORM』でCRUD APIを実装する

Docker環境の構築

Dockerを利用して検証環境を構築します。

実装の詳細は『ホットリロード x Go言語 x MySQL』なHTTPサーバのDocker環境構築手順で解説していますのであわせてご覧になってください。

Dockerfile

FROM golang:1.16.3-buster

WORKDIR /app
COPY . /app
RUN go mod tidy
RUN go get github.com/cosmtrek/air
CMD ["air"]

docker-compose.yml

version: '3'
services:
  app:
    build: .
    ports:
      - '3030:3000'
    volumes:
      - .:/app
    depends_on:
      - db
    command: ["./start.sh"]
  db:
    image: mysql:8.0.21
    ports:
      - '3306:3306'
    volumes:
      - go_mysql_data:/var/lib/mysql
      - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_USER: 'webuser'
      MYSQL_PASSWORD: 'webpass'
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'go_mysql8_development'
volumes:
  go_mysql_data:

start.sh

#!/bin/bash -eu
go mod tidy
air

MySQLのDockerイメージはコンテナ起動時に/docker-entrypoint-initdb.d配下のスクリプト(.sql.sh.sql.gz)を実行します。1
この機能を利用してサンプルデータを作成します。

docker-entrypoint-initdb.d/00_create_users.sql

DROP TABLE IF EXISTS users;

CREATE TABLE users (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(255),
  `email` VARCHAR(255),
  PRIMARY KEY (`id`)
);

INSERT INTO users (id, name, email) VALUES (1, "Yamada", "yamada@example.com");
INSERT INTO users (id, name, email) VALUES (2, "Tanaka", "tanaka@example.com");%

コンテナを起動する際にgo.modが必要になるため、go mod initでGo Modulesの初期化をします。

$ mkdir echo-gorm-crud-api-example cd $_

$ go mod init `basename $PWD`

$ tree -L 2
.
├── Dockerfile
├── docker-compose.yml
├── docker-entrypoint-initdb.d
│   └── 00_create.sql
├── go.mod
└── start.sh

EchoでHTTPサーバを実装する

EchoとはGo言語の軽量フレームワークです。特にREST APIをGo言語で実装する際に人気です。

まずはEchoで実装したHTTPサーバへ正しくリクエストが届くのか確認をします。
Echoの公式ドキュメント#Guideを参考に実装します。

main.go

package main

import (
  "net/http"

  "github.com/labstack/echo/v4"
)

func hello(c echo.Context) error {
  return c.String(http.StatusOK, "Hello, World!")
}

func main() {
  e := echo.New()
  e.GET("/", hello)
  e.Logger.Fatal(e.Start(":3000")) // コンテナ側の開放ポートと一緒にすること
}

動作確認

コンテナを起動してレスポンスが確認できればOKです。

### 作業ディレクトリへ移動
$ cd /path/to/working_directory

### 現在のディレクトリの内容を確認
$ tree -L 2
.
├── Dockerfile
├── docker-compose.yml
├── docker-entrypoint-initdb.d
│   └── 00_create_users.sql
├── go.mod
├── main.go
└── start.sh

### バックグラウンドで起動
$ docker-compose up -d

### レスポンスが返ってくればOK
$ curl localhost:3030
Hello, World!

GORMでデータベースにアクセスする

GORMとはGo言語のO/Rマッパです。Ruby on RailsのActive Recordに似た使い勝手のため、特にRuby経験者に人気です。

GORMのドキュメントを参考に、データベース接続の設定を追加します。

今回はデータベース接続に関するロジックを別パッケージで用意しました。
データベース接続の実装の詳細はGo言語でデータベース(MySQL)に接続する方法で紹介していますので併せてご覧になってください。
なお、GORMはV2.0とV1.0でデータベースの接続設定方法が異なるので注意が必要です。2

main.go

package main

import (
  "echo-gorm-crud-api-example/database"

  "net/http"

  "github.com/labstack/echo/v4"
)

func hello(c echo.Context) error {
  database.Connect()
  sqlDB, _ := database.DB.DB()
  defer sqlDB.Close()
  err := sqlDB.Ping()
  if err != nil {
    return c.String(http.StatusInternalServerError, "データベース接続失敗")
  } else {
    return c.String(http.StatusOK, "Hello, World!")
  }
}

func main() {
  e := echo.New()
  e.GET("/", hello)
  e.Logger.Fatal(e.Start(":3000"))
}

connect.go

package database

import (
  "fmt"
  "os"

  "github.com/joho/godotenv"
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

// DBを使い回すことで、DBへのConnectとCloseを毎回しないようにする
var DB *gorm.DB

func Connect() {
  err := godotenv.Load()
  if err != nil {
    panic(err.Error())
  }

  user := os.Getenv("DB_USER")
  password := os.Getenv("DB_PASSWORD")
  host := os.Getenv("DB_HOST")
  port := os.Getenv("DB_PORT")
  database_name := os.Getenv("DB_DATABASE_NAME")

  dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4"
  DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
  if err != nil {
    fmt.Println(err.Error())
  }
}


動作確認

データベースへ正常にアクセスできることを確認します。

### 作業ディレクトリへ移動
$ cd /path/to/working_directory

### 現在のディレクトリの内容を確認
$ tree -L 2
.
├── Dockerfile
├── database
│   └── connect.go
├── docker-compose.yml
├── docker-entrypoint-initdb.d
│   └── 00_create_users.sql
├── go.mod
├── main.go
└── start.sh

### バックグラウンドで起動
$ docker-compose up -d

### レスポンスが返ってくればOK
$ curl localhost:3030
Hello, World!

『Echo + GORM』でCRUD APIを実装する

ここからはEchoとGORMを利用してREST APIを実装する手順について紹介します。
今回はCRUD API(GET, PUT, POST, DELETE)を実装します。

GET API(全件取得)を実装する

レコードの構造体(User)を作成し、JSON形式でレスポンスを返します。
JSONのプロパティ名は構造体のタグ(json: "xxx")で定義します。

main.go

package main

import (
  "echo-gorm-crud-api-example/database"

  "net/http"

  "github.com/labstack/echo/v4"
)

type User struct {
  Id    int    `json:"id"`
  Name  string `json:"name"`
  Email string `json:"email"`
}

func getUsers(c echo.Context) error {
  users := []User{}
  database.DB.Find(&users)
  return c.JSON(http.StatusOK, users)
}

func main() {
  e := echo.New()
  database.Connect()
  sqlDB, _ := database.DB.DB()
  defer sqlDB.Close()

  e.GET("/users", getUsers)

  e.Logger.Fatal(e.Start(":3000"))
}

コンテナ起動後、curlでJSONレスポンスが取得できればOKです。

$ curl localhost:3030/users

[{"id":1,"Name":"Yamada","email":"yamada@example.com"},{"id":2,"Name":"Tanaka","email":"tanaka@example.com"}]

GET API(1件取得)を実装する

Echoには、構造体にタグを付与することでリクエストデータとテーブルのレコードをバインドする機能があります。3
この機能によって、たとえば『/users/:idというエンドポイントに/users/2というリクエストがきた場合、ID = 2のレコードを取得する』といったことが可能になります。

具体的なソースコードは以下の通りです。

main.go

package main

import (
  "echo-gorm-crud-api-example/database"

  "net/http"

  "github.com/labstack/echo/v4"
)

type User struct {
  Id int `json:"id" param:"id"`
  Name  string `json:"name"`
  Email string `json:"email"`
}

func getUsers(c echo.Context) error {
  users := []User{}
  database.DB.Find(&users)
  return c.JSON(http.StatusOK, users)
}

func getUser(c echo.Context) error {
  user := User{}
  if err := c.Bind(&user); err != nil {
    return err
  }
  database.DB.Take(&user)
  return c.JSON(http.StatusOK, user)
}

func main() {
  e := echo.New()
  database.Connect()
  sqlDB, _ := database.DB.DB()
  defer sqlDB.Close()

  e.GET("/users", getUsers)
  e.GET("/users/:id", getUser)

  e.Logger.Fatal(e.Start(":3000"))
}

コンテナ起動後、curlでJSONレスポンスが取得できればOKです。

$ curl localhost:3030/users/2

{"id":2,"name":"Tanaka","email":"tanaka@example.com"}

最終的なソースコード

PUTPOSTDELETEを追加した最終的なソースコードは以下の通りです。

main.go

package main

import (
  "go-docker-example/database"

  "net/http"

  "github.com/labstack/echo/v4"
)

type User struct {
  Id    int    `json:"id" param:"id"`
  Name  string `json:"name"`
  Email string `json:"email"`
}

func getUsers(c echo.Context) error {
  users := []User{}
  database.DB.Find(&users)
  return c.JSON(http.StatusOK, users)
}

func getUser(c echo.Context) error {
  user := User{}
  if err := c.Bind(&user); err != nil {
    return err
  }
  database.DB.Take(&user)
  return c.JSON(http.StatusOK, user)
}

func updateUser(c echo.Context) error {
  user := User{}
  if err := c.Bind(&user); err != nil {
    return err
  }
  database.DB.Save(&user)
  return c.JSON(http.StatusOK, user)
}

func createUser(c echo.Context) error {
  user := User{}
  if err := c.Bind(&user); err != nil {
    return err
  }
  database.DB.Create(&user)
  return c.JSON(http.StatusCreated, user)
}

func deleteUser(c echo.Context) error {
  id := c.Param("id")
  database.DB.Delete(&User{}, id)
  return c.NoContent(http.StatusNoContent)
}

func main() {
  e := echo.New()
  database.Connect()
  sqlDB, _ := database.DB.DB()
  defer sqlDB.Close()

  e.GET("/users", getUsers)
  e.GET("/users/:id", getUser)
  e.PUT("/users/:id", updateUser)
  e.POST("/users", createUser)
  e.DELETE("/users/:id", deleteUser)

  e.Logger.Fatal(e.Start(":3000"))
}

さいごに

最終的なソースコードはnishina555/echo-gorm-crud-api-exampleに公開しています。

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考