この記事は富士通株式会社FSWebユニット(旧株式会社富士通システムズウェブテクノロジー)が企画するいのべこ夏休みアドベントカレンダー 2021の11日目の記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。
概要
比較的最近に登場したネイティブコードコンパイラということで以前から興味をもっていたGO言語(GoLang)を少し勉強してみようと思い、GO言語で簡単なWebAPIを作ってみました。
マスコット・キャラクターはGopher(ホリネズミ)です。ゆるふわ系かな・・・??マスコットキャラクターの可愛さはDenoに軍配が上がりますね。
GO言語の特徴
少し調べたみたところGO言語には以下の特徴があるようです。
- C++の欠点を解消し、ストレスなくネイティブコードの開発ができる
- ネイティブコードであるため実行速度が速く、コンパイルも速い
- 複雑になりがちな機能をそぎ落としているため、文法がシンプル
作るもの
名言の登録/取得/削除ができるWebAPIを作ります。WebAPIはGoで作り、データはMySQLに保持します。どちらもDockerコンテナで起動する構成としています。
作成に際し、以下のサイトを大いに参考にさせていただきました。
Go+MySQL+Dockerで簡単なCRUDを実装する - Qiita
エンドポイント設計
一般的な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
- Go用のWebフレームワークです。
-
github.com/go-sql-driver/mysql
- GoからMySQLにアクセスするためのドライバーです。
-
github.com/jinzhu/gorm
- Go用のORMライブラリで、DBの操作を簡単に行うことができます。
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については説明を省略します。
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)を作る
参考にさせいただいたサイトそのままです。
[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()
で実装しています。
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 コマンドで起動します。
docker compose up -d
WebAPIを実行する
起動したWebAPIを目がけてリクエストを実行してみます。まずは、ブラームス先生の名言を登録してみます。RESTクライアントツールにはPostmanを使っています。
- URL : localhost:8080/meigens
- HTTPメソッド : POST
- リクエストボディ:
{ "meigen": "熟練の技がなければ、霊感などは風にそよぐ葦にすぎない。"}
続いて先ほどの名言が登録されているかを確認するために名言取得のAPIを実行してみます。
- URL : localhost:8080/meigens
- HTTPメソッド : GET
レスポンスから登録した名言が取得できていることを確認できました。
おわりに
GO言語の基本文法を学びつつ簡単なWebAPIを作ってみました。個人的にプログラミング言語はJavaやC#に慣れているため、GOの文法に戸惑いを感じているのが正直なところです。for文に()括弧がない、戻り値を複数返せる、try catchで例外処理を実装できない、などなど。慣れるまでにはまだ時間がかかりそうです。
また、GOの構造体にはタグによって実行時に参照可能なメタ情報を付与することができるそうで、このタグを上手く活用するのがGO言語で実装する上で重要のようです。これからもっとGO言語を学んでいきたいと思います。