feature image

2022年3月24日 | ブログ記事

GoでToDoリストを作ろう ! (Day-3)

前回 → 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を書きます !

目次

  1. 今日やること
  2. modelとrouterディレクトリの追加
  3. データベース(DB)設定を書く
  4. ルーティングを書く
  5. GETを実装
  6. Dockerを起動

今日やること

まず前回やったことの復習です。

前回やったことの復習

  1. ToDoリストの設計を考えた
  2. PostmanとDockerをインストールした
  3. Docker関連ファイルをコピペして、Docker環境を整えた

そして、今日やることはこちらです。

今日やること

  1. データベース(DB)設定やルーティングの設定をする
  2. Goを書き、タスク一覧を取得するAPIを実装する
  3. Dockerコンテナ内でサーバーを起動し、正常に動作するか確かめる

Githubのここに今日の完成品のコードがあります。ここをみながら進めましょう。
※ .gitignore(gitを使うときに書く) や README.md(説明書) はGoの動作に関係しないため、無視してください。

ディレクトリ構成

現在は

の構造になっていると思います

下のように、modelrouterという名前のフォルダ(ディレクトリといいます) を追加します。

一つの場所に、すべてのコードを書くと読みみづらくなってしまうため、コードを適度に分けます。その際に、

にざっくりとわけ実装していきます。この辺りの解説は[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)
}

を書きます。現時点でディレクトリ構造はこのようになっています。

先ほど追加した

を保存した際、importの付近に大量の赤い線が引かれている(エラー)と思います。この理由を説明します。

Goでは使用されるライブラリをgo.modgo.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、文字列のNamebool値の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です。
material_docker-1

Dockerコンテナを起動する

VScodeに戻り、プロジェクトのルートディレクトリで

docker compose up -d --build 

を実行します。

成功するとdockerコンテナが立ち上がります。

アプリ(server) が8000番(localhost:8000)に立ち上がります。

サーバーを起動する

Docker Desptopアプリからserverコンテナ内に入ります。
material_enter_container

go run main.go

を打ち、しばらくするとEchoが立ち上がります。

APIを叩く

Postmanを開き、GET http://localhost:8000/api/tasksを設定し、sendを押します。
material_postman_get

serverのコンテナに、<時刻> localhost:8000 GET /api/tasks 200 のログが表示され、空[]のレスポンスで返ってきたら成功です。

material_postman_get_responce

これは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)です。

irori icon
この記事を書いた人
irori

SysAd班、アルゴリズム班(Kaggle)とグラフィック班で活動しています

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年12月11日
DIGI-CON HACKATHON 2023『Mikage』
toshi00 icon toshi00
2022年4月7日
traPグラフィック班の活動紹介
annin icon annin
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2024年6月21日
ハッカソン参加記 4班"Slide Center"
Alt--er icon Alt--er
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記