feature image

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

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

前回 → https://trap.jp/post/1517/
次回 → (フロント編後編) https://trap.jp/post/1497/
Github → https://github.com/Irori235/ToDoList-Server/tree/day-4

21Bのいろりです。おいす〜。

この記事はサーバー編4日目です。今日で最終回を迎え、ToDoリストが完成します!

目次

  1. 今日やること
  2. POSTを実装
  3. PUTの実装
  4. DELETEの実装
  5. リファクタリング
  6. 今後の勉強について

今日やること

まずDay-3にやったことを復習しましょう。

前回やったことの復習

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

これを踏まえて、今日やることはこちらです。

今日やること

  1. 残りの機能を実装し、ToDoリストを完成させる
  2. 起動し、正常に動くか確認する
  3. 今後の勉強の流れを知る

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

POST (api/tasks)

前回のGETに引き続き、POSTを実装します。

APIを追記

router/router.goにAPIを追記します

router/router.go

// APIを書く場所
e.GET("/api/tasks", GetTasksHandler)
e.POST("/api/tasks", AddTaskHandler) //これを追記
追記後の 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を書く場所
	e.GET("/api/tasks", GetTasksHandler)
	e.POST("/api/tasks", AddTaskHandler)

	// 8000番のポートを開く(*2)
	err := e.Start(":8000")
	return err
}

AddTaskHandlerに赤線(エラー)が引いてあると思います。
カーソルをあてると、「undeclared name: AddTaskHandler」と書いてあります。
これを和訳すると「AddTaskHandlerなんてものは定義されていません💢 」になりますね。

ですから、AddTaskHandlerを定義していきます。

エラーの解決策

よく見ればエラーの原因が書かれているのに、 見落として悩み続けていることがよくあります。
これは問題文を読み飛ばして、解答形式を間違えるのと同じで、なかなか無くなりません。

エラーを意識的に読む習慣をつけましょう !
それでもわからない場合は、エラー文をコピペしてググると大抵のことは解決します。(マジ)

AddTaskHandlerを実装

関数AddTaskHandler を実装します。

POSTはbody にタスクの名前が入って送信されてくるのでした ([Day-2 APIの仕様策定])

その際にRequsetTask構造体を定義します。そのままだと長いため、ReqTaskとします。
関数 GetTaskhandlerの下に、

router/task.go

// ReqTask型は文字列のNameをパラメーターとして持つ
type ReqTask struct {
	Name string `json:"name"`
}

json:"name" とは jsonデータを代入するための識別子です。と、言われてもピンとこないと思うので、関数 AddTaskHandlerの実装を通して理解しましょう。

router/task.go

// 関数 AddTaskHandler は引数がecho.Context型で、戻り値はerror型である 
func AddTaskHandler(c echo.Context) error {
    
    // 空のReqTaskである、reqを定義
    var req ReqTask
    
    // bodyのjsonファイルをbind(*1)
	err := c.Bind(&req)  
    // エラーハンドリング(day-3のGetTaskHandlerを参照)
    // StatusBadRequestを返す
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
	}
    
    // 空のmodel(package)のTaskである、taskを定義
	var task *model.Task
    
    // model(package)のAddTask関数を実行し、戻り値をtask,errと定義
	task, err = model.AddTask(req.Name)
    // エラーハンドリング
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
	}
    
    // StastsOK と 追加されたtaskを返す
	return c.JSON(http.StatusOK, task)
}

を追記します。

追記後の router/task.go
package router

// 使用するライブラリをimport
import (
	"ToDoList-Server/model"
	"net/http"

	"github.com/labstack/echo/v4"
)

// ReqTask型は文字列のNameをパラメーターとして持つ
type ReqTask struct {
	Name string `json:"name"`
}

// 関数 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)
}

// 関数 AddTaskHandler は引数がecho.Context型で、戻り値はerror型である
func AddTaskHandler(c echo.Context) error {

	// 空のReqTaskである、reqを定義
	var req ReqTask

	// bodyのjsonファイルをbind(*1)
	err := c.Bind(&req)
	// エラーハンドリング(day-3のGetTaskHandlerを参照)
	// StatusBadRequestを返す
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
	}

	// 空のmodel(package)のTaskである、taskを定義
	var task *model.Task

	// model(package)のAddTask関数を実行し、戻り値をtask,errと定義
	task, err = model.AddTask(req.Name)
	// エラーハンドリング
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
	}

	// StastsOK と 追加されたtaskを返す
	return c.JSON(http.StatusOK, task)
}

AddTaskの実装

関数 GetTasksの下に、

model/task.go

// 関数 AddTask は引数がstring型のnameで、戻り値はTaskのポインターとerror型である (*1)
func AddTask(name string) (*Task, error) {
    
    // 新たなuuidを生成し、これをid、成否をerrとする(*2)
	id, err := uuid.NewUUID()
	if err != nil {
		return nil, err
	}
    
    // ID,Name,Finishedにid,name,false を代入したTask型のtaskを定義
	task := Task{
		ID:       id,
		Name:     name,
		Finished: false,
	}
    
    // taskをDBのTaskテーブルに追加。その成否を(ry
	err = db.Create(&task).Error
    
    // taskのポインタ と errを返す
	return &task, err
}

*1 ポインタは難しいので、 とりあえずこういう書き方だと動くんだ〜でOK。詳しい解説はここがわかりやすい。
[とってもやさしいGo言語入門]

*2 NewUUIDはライブラリgoogle.uuidの関数。関数めいの通り、新しいUUIDを生成する。

動作確認

プロジェクトのルートで

$ docker compose up -d --build

serverコンテナに入り、

go run main.go

Echoが起動したことを確認します。

Postmanを開き、POST  http://localhost:8000/api/tasks を設定し、body →raw →JSON を選択して、

{
    "name": "クリップを買う"
}

をセットして叩きます(Send)。

material_postman_post

serverのコンテナに<時刻> localhost:8000 POST /api/tasks 200 のログが表示され、
レスポンスで、新しく作られたtask型がjsonで返されて入れば成功です。

material_postman_post_responce

phpMyAdmin

DBはエクセルっぽい形をしていると書きました。それならエクセルっぽくDBの中身を表示するツールがないものか? と考えるでしょう。そういったツールの一つがphpmyAdminです。
見た目の古くささとは裏腹に、現在も継続的にアップデートされて、よく使われているツールです。

ブラウザで http://localhost:4040 にアクセスするとphpMyAdminが開きます。
左側のタブからTodolist →tasks を開くと、先ほど送信したタスクがDB上に登録してあるのがわかります。

material_phpmyadmin_tasks-1

動作確認が終わったら、

$ docker compose down

でコンテナを停止します。

PUT (api/tasks/:taskID)

続いて、PUTDELETEもゴリゴリ実装していきましょう。

router/router.go

// APIを書く場所
	e.GET("/api/tasks", GetTasksHandler)
	e.POST("/api/tasks", AddTaskHandler) 
    e.PUT("/api/tasks/:taskID", ChangeFinishedTaskHandler)  //これを追加

関数 AddTaskHandlerの下に、
router/task.go

func ChangeFinishedTaskHandler(c echo.Context) error {

    // taskIDのパスパラメータ(string型)を取得し、uuid型に変換。その値をtaskID、成否をerrとする
	taskID, err := uuid.Parse(c.Param("taskID"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
	}
    
    // 関数 ChangeFinishedTaskを実行、戻り値をerrに代入する(errを更新した)
	err = model.ChangeFinishedTask(taskID)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
	}
	return c.NoContent(http.StatusOK)
}

ここでもgoogle/uuid  を使ったため(uuid.Parse )、忘れずにimportを追記しておきましょう。

router/task.go

import (
	"ToDoList-Server/model"
	"net/http"

	"github.com/google/uuid" //これを追記

	"github.com/labstack/echo/v4"
)

続いて 関数AddTaskの下に、

model/task.go

// 関数 ChangeFinishedTaskの引数はuuid.UUID型のtaskIDで、戻り値はerror型である
func ChangeFinishedTask(taskID uuid.UUID) error {

    // DBのTaskテーブルからtaskIDと一致するidを探し、そのFinishedをtureにする(*3)
	err := db.Model(&Task{}).Where("id = ?", taskID).Update("finished", true).Error
	return err
}

(*3) Excelのクエリと似ていますね !  DBに格納されたデータを操作することをCRUDといいます。自分で新しいアプリを作る際に、DB上のデータ操作で困ったときは、GORMのドキュメントのCRUD Interfaceのところを覗いてみるといいでしょう。
[GORMガイド |GORM]

DELETE (api/tasks/:taskID)

今までの応用で全て書けます。是非やっていきましょう !

router/router.go

// APIを書く場所
	e.GET("/api/tasks", GetTasksHandler)
	e.POST("/api/tasks", AddTaskHandler) 
    e.PUT("/api/tasks/:taskID", ChangeFinishedTaskHandler)  
    e.DELETE("/api/tasks/:taskID", DeleteTaskHandler)  //これを追加

router/task.go

func DeleteTaskHandler(c echo.Context) error {
	taskID, err := uuid.Parse(c.Param("taskID"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
	}
	err = model.DeleteTask(taskID)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
	}
	return c.NoContent(http.StatusOK)
}

model/task.go

func DeleteTask(taskID uuid.UUID) error {
    // DBのTaskテーブルからtaskIDと一致するidを探し、そのタスクを削除する
	err := db.Where("id = ?", taskID).Delete(&Task{}).Error
	return err
}

動作確認

前回と同様にします。ファイルの変更を保存し、Dockerコンテナを起動して、serverコンテナに入りgo run main.goを打ちます。Echoが立ち上がったことを確認します。


GET

Postmanを開きGETを叩いて、前回登録したタスク「クリップを買う」が返されることを確認します。



POST

次に、タスク「デスク整理」をセットして、POSTします。

phpMyAdminに、「デスク整理」が登録されていることを確認し、タスク「デスク整理」のidをコピーします。



PUT

このid をhttp://localhost:8000/api/tasks/:taskId:taskIdの部分にペーストして、PUTを叩きます。:taskIdのようにして渡す値を、パスパラメーターと言います。

phpMyAdminでタスク「デスク整理」のfinishedが 1 (true)になっていることを確認します。



DELETE

同じようにして、DELETEを叩きます。

phpMyAdminで確認します。

動作確認が終わったら、コンテナを停止します。

リファクタリング

リファクタリングとは、コードの挙動を変えずに、コードを整理することです。

router/router.goを見てみましょう。APIのパスの指定が少々煩雑です。
全て api/tasksで始まるのに、毎回書いているのは読みにくいですね。

router/router.go

// APIを書く場所
e.GET("/api/tasks", GetTasksHandler)
e.POST("/api/tasks", AddTaskHandler)
e.PUT("/api/tasks/:taskID", ChangeFinishedTaskHandler)
e.DELETE("/api/tasks/:taskID", DeleteTaskHandler) 

を整理して、このように書くことができます。

// APIを書く場所
api := e.Group("/api")
	{
		apiTasks := api.Group("/tasks")
		{
			apiTasks.GET("", GetTasksHandler)
			apiTasks.POST("", AddTaskHandler)
			apiTasks.PUT("/:taskID", ChangeFinishedTaskHandler)
			apiTasks.DELETE("/:taskID", DeleteTaskHandler)
		}
	}

動作確認をしましょう !

今後の勉強について

4日間お疲れ様でした 🎉
今回やったことは、実はtraP内の「Webエンジニアになろう講習会」(通称、なろう) という講習会の最終課題です。なろう講習会は、脱落者が非常に多いことで有名です (私も脱落した)。
ですから、最後まで完走できた人は誇りを持っていいと思います。できなかったとしても、Web開発の雰囲気を体感していただけたら幸いです。

サーバーサイドでは、今回取り上げたGoの知識以外でも

などなど必要とされる知識は多岐に渡ります。いっぺんに勉強することは難しいので、自分でWebアプリを作りながらその都度調べていくのがいいのかなと思います。

また、Goについて深く知りたくなった場合には、[スターティングGo言語]が、よくおすすめされています。他に、ライブラリを一切使わずにWebアプリを作るのも非常に勉強になると言われています。これは[Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る (impress top gear)]を読むといいでしょう。

初心者を脱してきた方には、[みんなのGo言語]がおすすめです。

Web開発に興味が出てきたら、ぜひtraPのSysAd班で一緒に勉強しましょう ! それでは〜


謝辞

この記事を書くにあたって、mehm8128君、asari君、kounosuke君と先輩方にたくさんのアドバイスをいただきました。本当にありがとうございます。


明日は @logica さんです。楽しみ〜 !

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

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

この記事をシェア

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

関連する記事

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
2023年10月20日
DIGI-CON HACKATHON 参加記事「Comic DoQ」
mehm8128 icon mehm8128
2023年6月23日
2023 春ハッカソン 26班 『traP Mission』
Ras icon Ras
記事一覧 タグ一覧 Google アナリティクスについて