前回 → https://trap.jp/post/1302/
次回 → https://trap.jp/post/1518/
Github → https://github.com/Irori235/ToDoList-Server/tree/day-3
この記事は新歓ブログリレー14日目の記事です。
21Bのいろりです。Discordの進捗部屋でこれを書いています。お絵描きやWeb開発などやっていることに関わらず、わいわい雑談しながら進捗できるのが、traPの好きなところの一つです。
この記事はサーバー編3日目です。今日からはいよいよGoを書きます !
今日やること
まず前回やったことの復習です。
前回やったことの復習
- ToDoリストの設計を考えた
- PostmanとDockerをインストールした
- Docker関連ファイルをコピペして、Docker環境を整えた
そして、今日やることはこちらです。
今日やること
- データベース(DB)設定やルーティングの設定をする
- Goを書き、タスク一覧を取得するAPIを実装する
- Dockerコンテナ内でサーバーを起動し、正常に動作するか確かめる
Githubのここに今日の完成品のコードがあります。ここをみながら進めましょう。
※ .gitignore(gitを使うときに書く) や README.md(説明書) はGoの動作に関係しないため、無視してください。
ディレクトリ構成
現在は
の構造になっていると思います
下のように、model
とrouter
という名前のフォルダ(ディレクトリといいます) を追加します。
一つの場所に、すべてのコードを書くと読みみづらくなってしまうため、コードを適度に分けます。その際に、
- model : データベース(DB) 操作
- router : APIを受け取り、modelに受け渡す
にざっくりとわけ実装していきます。この辺りの解説は[Go初心者が「アプリ作っちゃうか〜」ってなった時に悩むやつ] に書かれています。
Echo, GORM
純粋なGoだけで書くと、実装がめんどくさい部分に出くわすことがあります。
その時に、めんどくさい事を裏でやってくれて、手軽に記述できるライブラリを導入します。フレームワークもライブラリの一種です。こう書くと強い人に怒られるかもしれませんが、とりあえずこの捉え方で大丈夫です。説明を簡単にするために、以後ライブラリに統一します。
EchoはAPIを受け取る箇所を、GORMはデータベース(DB) の操作全般を手軽にしてくれるライブラリです。
GORMはv1とv2で破壊的変更が入っています。破壊的変更とは、新しいバージョンでで新規機能が使えるようになるだけでなく、既存の方法が変更または廃止され、以前のやり方では動かなくなってしまう事をいいます。なんだか響きがかっこいいですね。
データベース(DB) 設定
Dockerの起動と同時に、データベース(DB) が作成されます。
APIを受け取った際に、操作するDBを指定してあげる必要があります。その設定を書きます。今回の記事の目的は、ロジックを書くことなので、コードの説明は避けコピペで済ませようと思います。
model/db.go
のファイルを作成し、以下のコードをコピペして下さい。
model/db.go
package model
import (
"database/sql"
"fmt"
"os"
_ "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var db *gorm.DB
// DB接続とテーブルを作成する
func DBConnection() *sql.DB {
dsn := GetDBConfig()
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(fmt.Errorf("DB Error: %w", err))
}
CreateTable(db)
sqlDB, err := db.DB()
if err != nil {
panic(fmt.Errorf("DB Error: %w", err))
}
return sqlDB
}
// DBのdsnを取得する
func GetDBConfig() string {
user := os.Getenv("DB_USERNAME")
password := os.Getenv("DB_PASSWORD")
hostname := os.Getenv("DB_HOSTNAME")
port := os.Getenv("DB_PORT")
dbname := os.Getenv("DB_DBNAME")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, password, hostname, port, dbname) + "?charset=utf8mb4&parseTime=True&loc=Local"
return dsn
}
// Task型のテーブルを作成する
func CreateTable(db *gorm.DB) {
db.AutoMigrate(&Task{})
}
をコピーし、保存します(win:ctrl
+S
mac:command
+S
)。ディレクトリ構造はこのようになります。
自分でDB接続を設定する際に、参考にしたい場合は下を参照してください。
解説
package model
import (
"database/sql"
"fmt"
"os"
_ "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var db *gorm.DB
// DB接続とテーブルを作成する関数
func DBConnection() *sql.DB {
// 関数GetDBConfigを実行し、戻り値をdsnと定義する
dsn := GetDBConfig()
// エラー型のerrを定義する
var err error
// dsnを使ってDBに接続する。戻り値をdbとerrに代入する
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(fmt.Errorf("DB Error: %w", err))
}
// Task型のテーブルを作成する
CreateTable(db)
// *gorm.DB型を *sql.DB型に変換する
sqlDB, err := db.DB()
if err != nil {
panic(fmt.Errorf("DB Error: %w", err))
}
// sqlDBを返す
return sqlDB
}
// DBのdsnを取得する関数
func GetDBConfig() string {
// 各種環境変数を読み込む(docker-compose.ymlで設定している)
user := os.Getenv("DB_USERNAME")
password := os.Getenv("DB_PASSWORD")
hostname := os.Getenv("DB_HOSTNAME")
port := os.Getenv("DB_PORT")
dbname := os.Getenv("DB_DBNAME")
// dsn(DBの接続情報につける識別子)を定義する
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, password, hostname, port, dbname) + "?charset=utf8mb4&parseTime=True&loc=Local"
// dsnを返す
return dsn
}
// テーブルを作成する関数
func CreateTable(db *gorm.DB) {
// Task型のテーブルを作成する
db.AutoMigrate(&Task{})
}
ルーティングを書く
APIが叩かれた時の処理の設定を記述します。ここも細かな設定がメインなため、流し読みで大丈夫です。
router/router.go
ファイルを作成し、以下をコピペする。
router/router.go
package router
import (
"os"
"github.com/labstack/echo/v4/middleware"
_ "net/http"
"github.com/labstack/echo/v4"
)
//Routingを設定する関数 引数はecho.echo型であり、戻り値はerror型
func SetRouter(e *echo.Echo) error {
// 諸々の設定(*1)
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339_nano} ${host} ${method} ${uri} ${status} ${header}\n",
Output: os.Stdout,
}))
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// APIを書く場所
// 8000番のポートを開く(*2)
err := e.Start(":8000")
return err
}
*1 middlewereの設定
ここでは、ログを出すといったオプションが記述されています。
とりあえず飛ばしてもらってOKです。自分でカスタマイズしたくなったら、下を読むといいと思います。
解説
// APIが叩かれた時にログを出す
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339_nano} ${host} ${method} ${uri} ${status} ${header}\n",
Output: os.Stdout,
}))
// 予想外のエラーが発生した際でも、サーバーを落とさないようにする
e.Use(middleware.Recover())
// CORSに対応する
e.Use(middleware.CORS())
これらはEchoのドキュメントに詳しく記載されています。
[Middleware|echo]
例えばLogならここに記載されています。
*2 ポートの設定
ポートとは、データのやり取りをする際に使う入り口です。ポートの解説は以下の記事がわかりやすいと思います。
[ポート(port)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典]
次にmain.go
に
main.go
package main
import (
"github.com/labstack/echo/v4"
)
func main() {
sqlDB := model.DBConnection()
defer sqlDB.Close()
e := echo.New()
router.SetRouter(e)
}
を書きます。現時点でディレクトリ構造はこのようになっています。
先ほど追加した
- model/db.go
- router/router.go
- main.go
を保存した際、importの付近に大量の赤い線が引かれている(エラー)と思います。この理由を説明します。
Goでは使用されるライブラリをgo.mod
やgo.sum
で管理します。ここで、importに書かれているのに関わらず、go.mod
に書かれていないものはエラーが出されます。
この時、プロジェクトのルートで
$ go mod tidy
を打つと、go.mod
が自動で適切に書き変わります。
これにより、大量のエラーが消え、「Taskが定義されていません💢」のエラーのみになると思います。この「Task」は今から定義します。
GET
/api/tasks
はい ! いよいよGoを本格的に書いていきます。
Goの基本的な書き方は [A Tour of Go]に譲りますが、 あまり理解していなくても大丈夫です。Basicを一通り目を通したぐらいなら十分です。
model/task.go
のファイルを作成します
import
このmodel/task.go
では、ライブラリのgoogle/uuidとGORM(v2)を使うため、これをimportします。
package model
import (
"github.com/google/uuid"
_ "gorm.io/gorm"
)
と書きます。
Task構造体を定義
次に、Task構造体を定義します。Task型といった方がわかりやすいかもしれませんね。
Todoアプリでは、タスクを追加したり、完了にしたり、削除したりします。これはタスク単位で動いていていることに注目しましょう。
先頭が大文字(*3)である必要があることに注意して下さい。
// Task型はuuid.UUID型のID、文字列のNameとbool値のFinishedをパラメーターとして持つ
type Task struct {
ID uuid.UUID
Name string
Finished bool
}
*3 Task型はmodel外でも使いたいのですが、先頭が大文字でないとmodel外(正確には package modelの外)から参照することができないためです。
データベース(DB) からtask一覧を取得
DBからtask一覧を取得する関数GetTasks
を書きます。
modle/task.go
// 関数GetTasksは、引数はなく、戻り値は[]Task型(Task型のスライス)とerror型である
func GetTasks() ([]Task, error) {
// 空のタスクのスライスである、tasksを定義する
var tasks []Task
// tasksにDBのタスク全てを代入する。その操作の成否をerrと定義する(*4)
err := db.Find(&tasks).Error
// tasksとerrを返す
return tasks, err
}
*4 tasksにDBのタスク全てを代入するのは、以下のように記述できます。
db.Find(&tasks)
この後ろに.Error
をつけると、成功した時にはnil(≒null)が、失敗時にはエラーの内容が返されます。それをerrに代入しています。
最後に保存して、go mod tidy
をします。エラーが消えてない時は、保存を忘れていないか確認しましょう。
エラーハンドリングについて
APIを叩いた際に、どこかしらでエラーが発生することがありますが、その際にエラーの原因を速やかに特定できるように、エラー発生時の例外処理を書きます。これをエラーハンドリングといいます。
この際にGoでは、一般的な戻り値に加え、処理に成功したかどうかを返却する手法を取っています。
Goのエラーハンドリングの戦略は、この記事がわかりやすいと思います。
[Goエラーハンドリング戦略]
Task一覧をjsonで返却する
model/task.go
と同じようにゴリゴリ実装していきましょう !
router/task.go
package router
// 使用するライブラリをimport
import (
"net/http"
"ToDoList-Server/model"
"github.com/labstack/echo/v4"
)
// 関数 GetTasksHandlerは引数がecho.Context型のc で、戻り値はerror型である
func GetTasksHandler(c echo.Context) error {
// model(package)の関数GetTasksを実行し、戻り値をtasks,errと定義する。
tasks, err := model.GetTasks()
// errが空でない時は StatusBadRequest(*5) を返す
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
// StasusOK と tasksを返す
return c.JSON(http.StatusOK, tasks)
}
*5 Statusについて
APIを叩いた際に、その処理が成功したかどうかをStatusで返します。Statusコードの詳細は下のページを参照して下さい。
[HTTP レスポンスステータスコード|mdn web docs_]
ルーティング
次に GET
/api/tasks が叩かれた時に、関数GetTasksHandler が実行されるようにします。
router/router.go
に移動します。
router/router.go
//APIを書く場所
の下に、
e.GET("/api/tasks", GetTasksHandler)
を書き、保存します。
router/router.go
は最終的にこのようになります。
package router
import (
"os"
"github.com/labstack/echo/v4/middleware"
_ "net/http"
"github.com/labstack/echo/v4"
)
//ルーティングを設定する関数 引数はecho.echo型のc であり、戻り値はerror型である
func SetRouter(e *echo.Echo) error {
// 諸々の設定(*1)
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339_nano} ${host} ${method} ${uri} ${status} ${header}\n",
Output: os.Stdout,
}))
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// APIを書く場所
e.GET("/api/tasks", GetTasksHandler)
// 8000番のポートを開く(*2)
err := e.Start(":8000")
return err
}
できました。起動して動作確認してみましょう !
起動
DockerDesktopアプリを開き、Dockerエンジンがrunning
であることを確認します。左下のクジラのマークの箇所が緑色になっていればOKです。
Dockerコンテナを起動する
VScodeに戻り、プロジェクトのルートディレクトリで
docker compose up -d --build
を実行します。
成功するとdockerコンテナが立ち上がります。
アプリ(server) が8000番(localhost:8000
)に立ち上がります。
サーバーを起動する
Docker Desptopアプリからserverコンテナ内に入ります。
go run main.go
を打ち、しばらくするとEchoが立ち上がります。
APIを叩く
Postmanを開き、GET
http://localhost:8000/api/tasks
を設定し、sendを押します。
serverのコンテナに、<時刻> localhost:8000 GET /api/tasks 200
のログが表示され、空[]
のレスポンスで返ってきたら成功です。
これはDB上にタスクがないため、空が返却されています。次回、DBにタスクを追加するAPIを実装しますが、タスクを追加した後にGET
を叩くと、DB上の全てのタスクがjson形式で返却されます。
また、実行と同時に、mysqlとphpMyAdminのディレクトリが作成されたと思います。これはdockerコンテナが停止しても、コンテナ内のデータが保存されるように、ここに記録されています。この保存領域をボリュームといいます。
言い換えれば、このファイルを削除すればDBはリセットされます。これは下の注意書きで書いてあります。
DB(mysql)がうまく動作しない場合
プロジェクトのルートで
$ rm -r mysql
を実行し、mysqlフォルダを削除してから、もう一度docker compose up -d --build
で実行しし直して下さい。
Dockerコンテナを停止する
プロジェクトのルートで
$ docker compose down
でコンテナを停止します。
今日はこれまでです。お疲れ様でした!
次回はいよいよタスクの追加、完了、削除を実装し、ToDoリストを完成させます !
明日は、@Tennessine_699君、@yukikurage君 の記事とToDoリストを作ろう ! の最終回(Day-4)です。