Go + MySQL + Docker で簡単なWebAPIを作ってみた

この記事は富士通株式会社FSWebユニット（旧株式会社富士通システムズウェブテクノロジー）が企画する[いのべこ夏休みアドベントカレンダー 2021](https://qiita.com/jugemsan/items/6c8b7324b9390a557774)の11日目の記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。

# 概要

比較的最近に登場したネイティブコードコンパイラということで以前から興味をもっていたGO言語（GoLang）を少し勉強してみようと思い、GO言語で簡単なWebAPIを作ってみました。
マスコット・キャラクターはGopher（ホリネズミ）です。ゆるふわ系かな・・・？？マスコットキャラクターの可愛さはDenoに軍配が上がりますね。

<img width="200" alt="Gopher.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220817/72cdfe94-6a3e-61d9-c701-9f811e2a6cc5.png">
by <a href="http://reneefrench.blogspot.jp/">Renée French</a>

# GO言語の特徴

少し調べたみたところGO言語には以下の特徴があるようです。

- C++の欠点を解消し、ストレスなくネイティブコードの開発ができる
- ネイティブコードであるため実行速度が速く、コンパイルも速い
- 複雑になりがちな機能をそぎ落としているため、文法がシンプル

# 作るもの

名言の登録／取得／削除ができるWebAPIを作ります。WebAPIはGoで作り、データはMySQLに保持します。どちらもDockerコンテナで起動する構成としています。
作成に際し、以下のサイトを大いに参考にさせていただきました。

[Go\+MySQL\+Dockerで簡単なCRUDを実装する \- Qiita](https://qiita.com/daiki-murakami/items/c8f9df8defc937e185ee)

# エンドポイント設計

一般的なRestful APIのお作法に沿ったエンドポイントにします。

| エンドポイント | HTTPメソッド | 説明 |
|:--|:--|:--|
| /meigens  | GET  | 名言の一覧取得 |
| /meigens/:id  | GET  | 名言の取得 |
| /meigens  | POST | 名言の登録 |
| /meigens/:id  | DELETE  | 名言の削除 |

# プロジェクト構成

プロジェクトの構成は以下の通りです。

```
go_webapi
├─ docker-compose.yml
├─ Dockerfile
├─ main.go
└─ db
   └─ my.cnf
```

# GOのDockerfileを作る

ベースイメージはgoの公式イメージを利用します。原因を追っていませんが、latestにして最新のイメージを利用すると、後述するソースコードのままだとWebAPI実行時にエラーが発生しました。また、パッケージを3つインストールしています。

- [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin)
    - Go用のWebフレームワークです。
- [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)
    - GoからMySQLにアクセスするためのドライバーです。
- [github.com/jinzhu/gorm](https://github.com/jinzhu/gorm)
    - Go用のORMライブラリで、DBの操作を簡単に行うことができます。

```Dockerfile
FROM golang:1.14

RUN mkdir /app
WORKDIR /app

RUN go get github.com/gin-gonic/gin
RUN go get github.com/go-sql-driver/mysql
RUN go get github.com/jinzhu/gorm

EXPOSE 8080

CMD ["go", "run", "main.go"]
```

# GOとMySQLのコンテナを立ち上げる docker-compose.yml を作る

servicesのgoがwebAPIのコンテナに関する定義です。`tty: true`はコンテナを起動させた際にコンテナが終了してしまうのを防ぐ目的で指定しますが、未記載でもコンテナは起動を継続しているようにも見えます。本当に必要な設定なのかは確信を持てていません。main.goをコンテナから参照できるようにするため`volumes`でカレントディレクトリをバインドしています。MySQLについては説明を省略します。

```docker-compose.yml
version: '3'
services:
  go:
    build: .
    tty: true
    volumes:
      - .:/app
    ports:
      - 8080:8080
    depends_on:
      - "db"
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: db
      MYSQL_USER: usr
      MYSQL_PASSWORD: password
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - mysql-data:/var/lib/mysql
      - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - 3306:3306
volumes:
  mysql-data:
    driver: local
```

# MySQLの設定ファイル（my.cnf）を作る

参考にさせいただいたサイトそのままです。

```my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_bin

default-time-zone = SYSTEM
log_timestamps = SYSTEM

default-authentication-plugin = mysql_native_password

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4
```
# WebAPIを main.go で作る

Meigenという構造体を作り、ORMのdb.AutoMigrateによってDBに反映しています。Meigenはgorm.ModelとstringのMeigenをフィールドに持つ構造体です。gorm.ModelはID, CreatedAt, UpdatedAt, DeletedAtをフィールドに持つ構造体です。IDは自動で主キーとして扱われます。
ginフレームワークでURLとHTTPメソッドに対応する処理を実装していて、戻り値はjsonで返却しています。データベースアクセス処理は、`sqlConnect()`で実装しています。

```main.go
package main

import (
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
)

type Meigen struct {
	gorm.Model
	Meigen string
}

func main() {
	db := sqlConnect()
	db.AutoMigrate(&Meigen{})
	defer db.Close()

	router := gin.Default()

	router.GET("/meigens", func(c *gin.Context) {
		db := sqlConnect()
		defer db.Close()

		var results []Meigen
		db.Order("created_at asc").Find(&results)

		meigens := []Meigen{}
		for _, v := range results {
			meigens = append(meigens, v)
		}

		c.JSON(http.StatusOK, gin.H{"meigens": meigens})
	})

	router.GET("/meigens/:id", func(c *gin.Context) {
		db := sqlConnect()
		defer db.Close()

		n := c.Param("id")
		id, err := strconv.Atoi(n)
		if err != nil {
			panic("id is not a number")
		}
		var meigen Meigen
		if db.First(&meigen, id).RecordNotFound() {
			c.JSON(http.StatusNotFound, "Not Found")
		} else {
			c.JSON(http.StatusOK, meigen)
		}
	})

	router.POST("/meigens", func(c *gin.Context) {
		db := sqlConnect()
		defer db.Close()

		var req Meigen
		c.BindJSON(&req)

		meigen := &Meigen{Meigen: req.Meigen}
		db.Create(meigen)

		c.JSON(200, meigen)
	})

	router.DELETE("/meigens/:id", func(c *gin.Context) {
		db := sqlConnect()
		defer db.Close()

		n := c.Param("id")
		id, err := strconv.Atoi(n)
		if err != nil {
			panic("id is not a number")
		}

		var meigen Meigen
		if db.First(&meigen, id).RecordNotFound() {
			c.JSON(http.StatusNotFound, "Not Found")
		} else {
			db.Delete(&meigen)
			c.JSON(http.StatusOK, meigen)
		}
	})

	router.Run()
}

func sqlConnect() (database *gorm.DB) {
	DBMS := "mysql"
	USER := "usr"
	PASS := "password"
	PROTOCOL := "tcp(db:3306)"
	DBNAME := "db"

	CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo"

	count := 0
	db, err := gorm.Open(DBMS, CONNECT)
	if err != nil {
		for {
			if err == nil {
				fmt.Println("")
				break
			}
			fmt.Print(".")
			time.Sleep(time.Second)
			count++
			if count > 180 {
				fmt.Println("")
				panic(err)
			}
			db, err = gorm.Open(DBMS, CONNECT)
		}
	}

	return db
}
```

# コンテナを起動する

docker-compose.ymlのディレクトリに移動して以下のdocker compose コマンドで起動します。

```shell
docker compose up -d
```

# WebAPIを実行する

起動したWebAPIを目がけてリクエストを実行してみます。まずは、ブラームス先生の名言を登録してみます。RESTクライアントツールにはPostmanを使っています。

- URL : localhost:8080/meigens
- HTTPメソッド : POST
- リクエストボディ： `{ "meigen": "熟練の技がなければ、霊感などは風にそよぐ葦にすぎない。"}` 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220817/4aa0a59d-3b99-96e6-a27a-001ebd5a8a1a.png)

続いて先ほどの名言が登録されているかを確認するために名言取得のAPIを実行してみます。

- URL : localhost:8080/meigens
- HTTPメソッド : GET

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220817/12964160-6374-177e-dc8d-2caf8c015570.png)

レスポンスから登録した名言が取得できていることを確認できました。

# おわりに

GO言語の基本文法を学びつつ簡単なWebAPIを作ってみました。個人的にプログラミング言語はJavaやC#に慣れているため、GOの文法に戸惑いを感じているのが正直なところです。for文に()括弧がない、戻り値を複数返せる、try catchで例外処理を実装できない、などなど。慣れるまでにはまだ時間がかかりそうです。
また、GOの構造体にはタグによって実行時に参照可能なメタ情報を付与することができるそうで、このタグを上手く活用するのがGO言語で実装する上で重要のようです。これからもっとGO言語を学んでいきたいと思います。