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
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
2024年3月15日
個人開発として2週間でWebサービスを作ってみた話 〜「LABEL」の紹介〜
Natsuki icon Natsuki
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記