はじめに
先日、「業務で使う予定ないとはいえGoぐらいある程度知っておいたほうがいいよな...」と思い、Goを使って簡単なCRUDを実装してみたのでそのやり方を備忘録としてまとめておきます。
基本的には以下のサイトの内容を組み合わせて少しアレンジしたものになっています。Goの勉強をする上でこれらのサイトには非常にお世話になったのでこちらもご参考ください。
DockerでGoの開発環境を構築する
Go / Gin で超簡単なWebアプリを作る
Go言語(Golang)入門~MySQL接続編~
docker-compose MySQL8.0 のDBコンテナを作成する
docker-compose upでMySQLが起動するまで待つ方法(2種類紹介)
GoをDockerで立ち上げる
まずはGoをDockerで立ち上げていきます。
作業ディレクトリ直下に
- DockerFile
- docker-compose.yml
- main.go
を作成します
FROM golang:latest
RUN mkdir /app
WORKDIR /app
version: '3'
services:
go:
build:
context: .
dockerfile: DockerFile
command: /bin/sh -c "go run main.go"
stdin_open: true
tty: true
volumes:
- .:/app
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
これでdocker-compose up
を行うとコンソール上にHello, World!と出てくると思います。
これが出ればまずGoの起動は成功です。
簡単に各ファイルの解説をします。
・DockerFile
Goのコンテナ(仮想環境)を作成します。
ここでWORKDIR /app
を指定していることで以降の動作をすべて/app
以下で行ってくれます。
・docker-compose.yml
DockerFileで作ったコンテナを立ち上げるときの設定などを書きます。
これにより、DockerFileにあるコンテナを立ち上げてその中でgo run main.go
のコマンドを叩いてmain.goを起動します
・main.go
Goに関する処理はここに書いていきます。今回はHello, World!を出力するだけで終了しています。
GoでWebページを作成する
とりあえずGoの起動ができたので次はGoを使ってWebページを作っていきましょう。
今回はGinというフレームワークを使ってみます。
- DockerFileにインストールを追加
- docker-compose.ymlにportsの記述を追加
- main.goの内容を書き換え
- templates/index.htmlを作成
を行ってください。
FROM golang:latest
RUN mkdir /app
WORKDIR /app
RUN go get github.com/gin-gonic/gin
version: '3'
services:
go:
build:
context: .
dockerfile: DockerFile
command: /bin/sh -c "go run main.go"
stdin_open: true
tty: true
volumes:
- .:/app
ports:
- 8080:8080
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*.html")
router.GET("/", func(ctx *gin.Context){
ctx.HTML(200, "index.html", gin.H{})
})
router.Run()
}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Sample App</title>
</head>
<body>
<h1>Hello World!!!</h1>
</body>
</html>
これで
docker-compose build
docker-compose up -d
を行ってしばらく待ってから
http://localhost:8080にアクセスするとHello World!と表示されると思います。
今回やったことを解説します。
今回はGinというフレームワークを追加しました。
GinはDockerFileでコンテナ作成後にgo get github.com/gin-gonic/gin
というコマンドでインストールされ、main.goで呼び出されます。
そしてmain.goの中でtemplatesの中身が読み取られ、
router.GET("/", func(ctx *gin.Context){
ctx.HTML(200, "index.html", gin.H{})
})
によってroot("/")に対してtemplates/index.htmlが紐づけられることになります。
ちなみにrouter.GETの第一引数("/")を"/test"などに変えると、http://localhost:8080ではなく、http://localhost:8080/testでindex.htmlが表示されるようになります。
最後にdocker-compose.ymlにportを追加することでlocalhost:8080へのアクセスを可能にしています。
DockerでMySQLを起動する
ここまででGoでのWebページ作成はできるようになりました。しかし、実際にはWebサービスを作るときにDBとの接続は避けて通れない内容になってきます。
そこで次はDockerを使ってMySQLを立ち上げていきます。
まずはdocker-compose.ymlに
・dbコンテナについての記述
・volumeの記述
を追加してください。
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: go_database
MYSQL_USER: go_test
MYSQL_PASSWORD: password
TZ: 'Asia/Tokyo'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
- db-data:/var/lib/mysql
- ./db/my.cnf:/etc/mysql/conf.d/my.cnf
ports:
- 3306:3306
volumes:
db-data:
driver: local
また、dbディレクトリを作り、その中に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
(この辺は参考ページそのままです。ログに関する部分だけなぜか上手くいかなかったので外しています)
ここまでやってdocker-compose up -d
をやるとMySQLのコンテナも立ち上がるはずです。
設定の記述しかないのでここの説明は省略します。
GoとMySQLを接続する
MySQLが立ち上がったので早速Goにつないでみます。
今回は接続にsqlドライバーとGORMというフレームワークを使います。
- DockerFileにインストールの追加
- docker-compose.ymlに依存関係の記述を追加
- main.goにDB接続の処理を追加
を行ってください。
FROM golang:latest
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
version: '3'
services:
go:
build:
context: .
dockerfile: DockerFile
command: /bin/sh -c "go run main.go"
stdin_open: true
tty: true
volumes:
- .:/app
ports:
- 8080:8080
depends_on:
- "db"
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: go_database
MYSQL_USER: go_test
MYSQL_PASSWORD: password
TZ: 'Asia/Tokyo'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
- db-data:/var/lib/mysql
- ./db/my.cnf:/etc/mysql/conf.d/my.cnf
ports:
- 3306:3306
volumes:
db-data:
driver: local
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db := sqlConnect()
defer db.Close()
router := gin.Default()
router.LoadHTMLGlob("templates/*.html")
router.GET("/", func(ctx *gin.Context){
ctx.HTML(200, "index.html", gin.H{})
})
router.Run()
}
func sqlConnect() (database *gorm.DB) {
DBMS := "mysql"
USER := "go_test"
PASS := "password"
PROTOCOL := "tcp(db:3306)"
DBNAME := "go_database"
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("")
fmt.Println("DB接続失敗")
panic(err)
}
db, err = gorm.Open(DBMS, CONNECT)
}
}
fmt.Println("DB接続成功")
return db
}
これでdocker compose up
を行い、コンソールに「DB接続成功」と出たら成功です。
追加された内容はsqlConnectがメインなのでそこを解説します。
func sqlConnect() (database *gorm.DB) {
DBMS := "mysql"
USER := "go_test"
PASS := "password"
PROTOCOL := "tcp(db:3306)"
DBNAME := "go_database"
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("")
fmt.Println("DB接続失敗")
panic(err)
}
db, err = gorm.Open(DBMS, CONNECT)
}
}
fmt.Println("DB接続成功")
return db
}
前半部はDBに接続するための情報を定義しています。docker-compose.ymlで設定した内容を入力してください。
その後、db, err := gorm.Open(DBMS, CONNECT)
でDBに接続します。しかし、MySQLの起動時間によってはこのコマンドが実行される時点でMySQLの準備が完了していない場合があります。
そこでこのコードでは2つの対策をしています。
1つめはdocker-compose.ymlでの依存関係の設定です。
ここでdepends_onを設定したことにより、dbコンテナが立ち上がってからgoコンテナが立ち上がるようになります。
2つめはリトライ処理です。
dbコンテナが起動してからもMySQLが立ち上がるまでに時間がかかるのでもしDBにつながらなかった場合に1秒待ってからリトライするようにしています。
これだと本当にエラーのときにリトライし続けてしまうので適当な回数でエラーを返すようにします。このコードでは3分つながらなかったらエラーになるようになっています。
CRUDを実装する
ついにMySQLにもつながるようになったので最後にCRUDの処理を実装して実際の流れをみていきましょう。
あと変更するのはmain.goとindex.htmlのみです。
- Userの定義を作成
- マイグレーション
- post処理の実装
- ユーザー追加フォーム、ユーザー削除ボタンの実装
をやっていきます。
package main
import (
"fmt"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
gorm.Model
Name string
Email string
}
func main() {
db := sqlConnect()
db.AutoMigrate(&User{})
defer db.Close()
router := gin.Default()
router.LoadHTMLGlob("templates/*.html")
router.GET("/", func(ctx *gin.Context){
db := sqlConnect()
var users []User
db.Order("created_at asc").Find(&users)
defer db.Close()
ctx.HTML(200, "index.html", gin.H{
"users": users,
})
})
router.POST("/new", func(ctx *gin.Context) {
db := sqlConnect()
name := ctx.PostForm("name")
email := ctx.PostForm("email")
fmt.Println("create user " + name + " with email " + email)
db.Create(&User{Name: name, Email: email})
defer db.Close()
ctx.Redirect(302, "/")
})
router.POST("/delete/:id", func(ctx *gin.Context) {
db := sqlConnect()
n := ctx.Param("id")
id, err := strconv.Atoi(n)
if err != nil {
panic("id is not a number")
}
var user User
db.First(&user, id)
db.Delete(&user)
defer db.Close()
ctx.Redirect(302, "/")
})
router.Run()
}
func sqlConnect() (database *gorm.DB) {
DBMS := "mysql"
USER := "go_test"
PASS := "password"
PROTOCOL := "tcp(db:3306)"
DBNAME := "go_database"
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
}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Sample App</title>
</head>
<body>
<h2>ユーザー追加</h2>
<form method="post" action="/new">
<p>名前<input type="text" name="name" size="30" placeholder="入力してください" ></p>
<p>メールアドレス<input type="text" name="email" size="30" placeholder="入力してください" ></p>
<p><input type="submit" value="Send"></p>
</form>
<h2>ユーザー一覧</h2>
<table>
<tr>
<td>名前</td>
<td>メールアドレス</td>
</tr>
{{ range .users }}
<tr>
<td>{{ .Name }}</td>
<td>{{ .Email }}</td>
<td>
<form method="post" action="/delete/{{.ID}}">
<button type="submit">削除</button>
</form>
</td>
</tr>
{{ end }}
</ul>
</body>
</html>
これでdocker-compose up -d
を行い、http://localhost:8080にアクセスするとユーザー登録フォームが現れ、ユーザーを登録すると下に登録したユーザーの情報が表示されるようになります。
また、コンテナを削除して上げ直しても登録されたユーザーは削除されず、ユーザー一覧に表示が残るようになります。
それでは追加部分の解説をしていきます。
まず、main.goでUserという構造体を作成しています。gorm.Model
でidなどモデルに必要な内容をUserに入れ、更にUser特有のname, emailを追加しています。
この構造はdb.AutoMigrate
によってDBに反映されます。
続いて各パスでのCRUD処理を実装していきます。
rootパスではユーザー一覧を取得します。
db.Find(&users)でDB内にあるユーザー一覧をUser構造として取得します。
間にOrderを挟むことで取得時に古いユーザーが上に来るようにしています。
最後に取得したユーザーをindex.htmlに渡しています。
/newパスではフォームの内容をもとにユーザーを作成しています。
ctx.PostFormでフォームによってsubmitされた内容を取得し、その内容をdb.Createで永続化しています。
処理が終わったらrootにリダイレクトします。
/deleteパスではidを指定してユーザーを削除しています。
こちらではURLにユーザーのidを指定しているのですが、同様にctxから取得します。
そしてその内容からdb.Firstでユーザーを取得し、db.Deleteで該当のユーザーを削除します。
ここで、idはstringで渡されているのでstrconv.Atoiでint型に変換していることに注意してください。
index.htmlでは一般的なhtmlの書き方でformとtableを作成しています。
ここで、{{ range .users }}
という形でmain.goから渡されたusersを受け取っています。
おわりに
今回はGoでのWebサービス開発の導入としてGo+MySQL+Dockerで簡単なCRUDを実装してみました。あくまで練習なのでバリデーションとか細かい制御などは考えていません。
今回行った内容は初歩ではありますが、この内容を広げて複雑化していくことで実際にWebサービスを作ることができると思います。
もしGoで何か作ってみたいと考えている方がいらっしゃったらぜひ参考にしてみてください!