こんにちは、心肺機能を鍛えれば長生きすると思っているエンジニアのザワです。
エンジニアは常に学ぶ必要がある生き物だと私は思います。私はここ何年かフロントエンドを中心に開発をしてきました。しかし、Webアプリケーション開発をしているとバックエンドの知識が必要になることがしばしばあり、バックエンドに詳しい人の話を聞いていると羨望の眼差しで見つめてしまう私がいます。
最近はGo言語を勉強し始めたので、修行がてらToDoリストのAPIを作ってみました。制作工程の中で共有したくなったことをつらつら書いていこうと思います。
目次
なぜToDoリストを作るのか
理由は簡単、すぐ完成するからです。
完成することが成功体験となって自信に繋がると信じています。最初はブログの管理画面でも作るつもりでしたが、先が遠く見えなくなりそうだったので光の速さで方向転換しました。
最初から難しいアプリケーションを作ろうとすると、進みが見えづらかったり、わからないことが多すぎて勉強するのが苦痛になってしまいます。
作ってみて思ったのですが、ToDoリストは自分次第で機能盛り盛りにすることもできるので、最初の要件は最小限にすると尚良いなと思いました。要は作り切って形にすることが大事だと思います。
言語の基礎力アップするためにしたこと
Go言語については勉強始めたてなので、いきなりAPIを作ろうとしても書き方がわかりません。先人はたくさんいるので、記事を検索して参考にしながら進めるのはそうなんですけど、言語の基礎も同時に学びたいと思いました。
私の場合本や記事を読むより、動画で見たほうが吸収が良いので昼休みや寝る前、移動時間などの隙間時間を利用しました。
また、YouTubeではなくUdemyを利用しました。理由は知り合いにオススメされたのもありますが、今まで使ったことがないので刺激になるかなと思ったからです。内容は基礎文法を解説してくれるコースもあれば、簡単なハンズオンが用意されているものなどもあり、選ぶのが楽しいです。
ToDoリストの制作手順
では本題に入ります。
ToDoリストをどう作っていったのか、流れを紹介していきます。
ステップに分けてみると8つになりました。1日1つのペースで進めれば約1週間で終わります。ものによっては1日では終わらないものもありましたが、大枠この進め方で順調に進んだので良かったかなと思います。
- Go言語のインストール
- Helloと言わせる
- APIサーバを用意する
- Helloと言わせるエンドポイントを作る
- データベースサーバを用意する
- ToDoのモデルを作成する
- マイグレーションを実行してテーブル作成
- CRUD作成
各手順について説明していきます。
Go言語のインストール
私はフロントエンドがメインなので、Node.jsには親しみがあります。Node.jsのバージョン管理にはnodebrewを使っています。
同様にGo言語にもバージョン管理ツールがあるだろうなと探したらgoenvなるものがやっぱりありました。まずgoenvをインストールしてから使用するバージョンをインストールしました。
Helloと言わせる
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
いわゆる「Hello World」から始めました。
慣れている言語とは勝手が違うので、Hello Worldだけでも学ぶことがたくさんありました。
- プログラムの実行方法
- ターミナルへ出力する関数
- packageやimportなどのお作法
APIサーバを用意する
version: '3'
services:
app:
build:
context: .
ports:
- '8080:8080'
FROM golang:alpine3.15
WORKDIR /api
COPY ./main.go .
RUN go mod init todo && go mod tidy
CMD ["go", "run", "./main.go"]
DockerでAPIサーバを起動して、Helloを出力するところまで実装しました。
最小限にするならDockerは使わなくても良いのですが、データベースサーバも用意する想定でしたし、仮想環境上で作りたいなと思ったのでDockerで作ることにしました。
Dockerは普段から業務で触っていますが、一から設定を書いた経験はあまりないので必要最小限の設定だけ適用して、足りない機能は時間があるときに改良することにしました。
最小限の設定にすれば、各設定について調べる余裕ができます。
Helloと言わせるエンドポイントを作る
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/", hello).Methods(http.MethodGet)
http.ListenAndServe(":8080", r)
}
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello")
}
次は、リクエストしたらHelloとレスポンスが返ってくるところまで実装しました。ここは必要な知識が急に膨らむので色々調べながら試行錯誤して進めました。
この段階で外部パッケージの存在を知り、モジュール管理の存在を知ることとなりました。
データベースサーバを用意する
version: '3'
services:
app:
build:
context: .
ports:
- "8080:8080"
mysql:
image: mysql:5.7
environment:
- MYSQL_USER=xxx
- MYSQL_PASSWORD=xxx
- MYSQL_ROOT_PASSWORD=xxx
- MYSQL_DATABASE=todo
ports:
- "3306:3306"
mysqlのコンテナにアクセスしてデータベースが存在することを確認しました。
ToDoのモデルを作成する
type Todo struct {
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
Name string
}
色々調べている内にORマッパーであるGORMと出会いました。GORMを使うと定義したモデルをもとにマイグレーションを行いテーブル作成してくれるので楽そうだなと思い採用しました。
マイグレーションを実行してテーブル作成
func dbConnect() *gorm.DB {
dsn := "xxx:xxx@tcp(mysql:3306)/todo?parseTime=true"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
return db
}
func main() {
db := dbConnect()
db.AutoMigrate(&Todo{})
}
マイグレーションはAutoMigrate関数を実行するだけで完了します。なんて便利なんだと感激しましたが、最初はマイグレーションが上手くいきませんでした。
どうやらmysqlが起動する前にデータベースに接続しようとしているのが原因のようだったので、調べたところmysqlが起動するまでアプリケーション側の実行を待つ工夫が必要でした。
CRUD作成
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
var err error
type Todo struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"`
Name string `json:"name"`
}
func dbConnect() *gorm.DB {
dsn := "xxx:xxx@tcp(mysql:3306)/todo?parseTime=true"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
return db
}
func main() {
engine := gin.Default()
db := dbConnect()
db.AutoMigrate(&Todo{})
// create
engine.POST("/todo", func(c *gin.Context) {
todo := Todo{}
c.BindJSON(&todo)
if err := db.Create(&todo).Error; err != nil {
fmt.Println(err)
}
c.JSON(http.StatusOK, todo)
})
// read
engine.GET("/todo/:id", func(c *gin.Context) {
todo := Todo{}
id := c.Param("id")
if err := db.First(&todo, id).Error; err != nil {
fmt.Println(err)
}
c.JSON(http.StatusOK, todo)
})
// update
engine.PUT("/todo/:id", func(c *gin.Context) {
todo := Todo{}
id := c.Param("id")
if err := db.Where("id = ?", id).First(&todo).Error; err != nil {
fmt.Println(err)
}
c.BindJSON(&todo)
db.Save(&todo)
c.JSON(http.StatusOK, todo)
})
// delete
engine.DELETE("/todo/:id", func(c *gin.Context) {
todo := Todo{}
id := c.Param("id")
if err := db.Where("id = ?", id).First(&todo).Error; err != nil {
fmt.Println(err)
}
db.Delete(&todo)
c.JSON(http.StatusOK, todo)
})
engine.Run(":8080")
}
ついにメインであり最後の手順になります。この手順を楽しみに今まで頑張ってきました。
CRUDという言葉を知ったのは最近だったと思います。Webアプリケーション制作に携わるようになり、周りでそういった言葉が行き交う環境にいるので、いつの間にか自然と知っていました。
一応CRUDとは、Create、Read、Update、Deleteのことを指します。実装はというと、前述したGORM、それからGinというWebフレームワークのおかげでだいぶコンパクトに書けたと思います。
時間が掛かったのはGinについて調べたことと、Updateの実装です。どう更新すればよいのか試行錯誤してなんとか実装できました。
今後の課題
今回は一旦作り上げることを目指していたので必要最小限の要件で進めました。実装しながら今後の課題としてメモしていたものをまとめてみました。一つひとつが割と時間が掛かりそうなテーマだなと感じています。
- ログのフォーマットはどういうのが良いのだろう?
- 良いエラーハンドリングとはどういうのだろう?
- テスト書きたい
- リクエスト値のバリデーションしなきゃ
- golang-standardsに合わせたディレクトリ構成にしてみたい
おわりに
ToDoリストはフロントエンドで試すことはこれまでもあったのですが、バックエンドを実装したのは初めてでした。実装してみてあらためて思ったのは、「まず実装してみる」精神が大事だということです。
座学は基礎力をアップするのに大事だと思いますが、手を動かして悩むのがやはり一番覚えます。せっかくAPIを作ったので、今度はフロントエンドの勉強がてら、画面を作ろうかなと思っています。
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。