いたるまで

  1. Go Lang で WebAPI を作成するために、まずは Docker で MySQL を構築する
  2. Go Lang で WebAPI を作成するために、Golang で MySQL に接続する
  3. Go Lang で WebAPI を作成するために、Golang でサーバーを立ち上げる
  4. Go Lang で WebAPI を作成するために、Golang でエンドポイントにアクセスし DB からデータを取得する
  5. Go Lang で WebAPI を作成するために、Golang で DB のデータを取得する main ファイルをそれぞれの責務に分割する

ディレクトリ構成

.
├── db
├── db.go
├── handlers
│   ├── auth
│   │   ├── delete_user_handler.go
│   │   └── create_user_handler.go
│   └── handlers.go
├── models
│   └── user_model.go
└── main.go

上記のように作成した。

簡単に説明すると、

db/db.go

データベースとの接続が行われる。

handlers/handlers.go

HTTP リクエストに対する処理を管理している。

handlers/auth/handlers.go

ユーザー作成・削除に対する処理を作成している。

handlers/models/user_model.go

ユーザーに関する構造体を定義している。 データーベースモデル。

処理を追いかける

---
// main.go
---

package main

import (
	"log"

	"GiveAndTakeCollection-Backend/src/db"
	"GiveAndTakeCollection-Backend/src/handlers"
	"GiveAndTakeCollection-Backend/src/models"

	"github.com/joho/godotenv"
)

func init() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	db.InitDB()
	db.DB.AutoMigrate(&models.User{})
}

func main() {
	handlers.HandleRequests()
}

データーベースと接続する

プログラム初期化時に init 関数が実行

まずプログラム初期化時にinit関数が実行される。

これはGolangの特殊仕様。

godotenv.Load()godotenvライブラリを使用し、.envを読み込む。

エラーが出た際はerrにエラーオブジェクトをセットする。

if err != nilは Go 言語においてエラー処理を行う際の一般的な書き方。

envが読み込めなかった場合、log.Fatal("Error loading .env file")が実行される。

実行されることで、メッセージをログに記録し、それからプログラムを即座に終了する。

log.Fatal()は致命的なエラーが発生した場合に使われるログ出力関数らしい。

db.InitDB()が実行される

問題がなければdb.InitDB()が実行される。

こちらはインポートされた"GiveAndTakeCollection-Backend/src/db"InitDB関数が呼ばれる。

---
// db/db.go
---
package db

import (
	"fmt"
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB


// 不要?
type Model struct {
	gorm.Model
}

func InitDB() {
	dsn := os.Getenv("DB_CONNECTION_STRING")
	if dsn == "" {
		fmt.Println("DB_CONNECTION_STRING 環境変数が設定されていません")
		os.Exit(1)
	}

	var err error
	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		fmt.Printf("データベースへの接続に失敗しました: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("データベースに接続しました")
}

先頭でpackage dbと定義しているため、db.InitDB()で呼び出すことができる。

このファイルではまず、gorm.DB型のポインタ変数DBを宣言している。これはGormのデータベース接続を表す変数。

ちなみに、ポインタ変数というのは「変数のメモリアドレスを格納するための特別な型の変数」ということらしい。

変数にはデータそのものが格納されますが、ポインタ変数にはメモリアドレスが格納されます。

とのこと。

ちなみにGoでは下記のように宣言する。

var x int  // 通常の変数
var ptr *int  // int型のポインタ変数

詳細な使い方は調べる必要がある。

あとvar DB *gorm.DBパッケージレベルで行う方がいいらしい。

var DB *gorm.DB の宣言が initDB 関数の中で行われる場合、その変数はローカル変数として initDB 関数のスコープ内にのみ存在します。この場合、他の関数やファイルから DB 変数にアクセスすることができなくなります。

一般的に、データベース接続などのリソースはアプリケーション全体で共有されることが多いため、var DB *gorm.DB の宣言は関数の外、パッケージレベルで行われることがあります。これにより、他の関数やファイルから DB 変数にアクセスでき、アプリケーション内で一貫性のあるデータベース接続が確保されます。

話を戻して次に、db.InitDB()の処理を追いかける。

	dsn := os.Getenv("DB_CONNECTION_STRING")
	if dsn == "" {
		fmt.Println("DB_CONNECTION_STRING 環境変数が設定されていません")
		os.Exit(1)
	}

dsnという変数に、os.Getenv("DB_CONNECTION_STRING")で取得した環境変数を格納する。

os.Getenvの戻り値の型はstringなので、もし環境変数が空文字であれば、エラーメッセージを標準出力し、プログラムを終了する。

ちなみに引数は下記の通り。

os.Exit(0): プログラムの正常な終了を示します。
os.Exit(1): プログラムがエラーで終了したことを示します。

一般的には非ゼロの終了コードはエラーを示す慣習とされています。

環境変数が適切に設定されていた場合は下記コードが実行される。

	var err error
	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		fmt.Printf("データベースへの接続に失敗しました: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("データベースに接続しました")

ここでは、まずエラー変数を定義し、Gormを使用してMySQLデータベースに接続する。

成功するとDBに接続が設定され、失敗するとエラーがエラー変数に格納される。

if err != nil { ... }でエラーチェックが実行され、エラーの場合はエラーメッセージを標準出力し、プログラムを終了する。

問題がなければ、データベースに接続しましたが標準出力に表示される。

ここまでで、データベースの接続が完了し、次にmain関数が呼ばれる。

HTTP リクエストを処理するためのルーティングとサーバーの起動

main 関数が実行される

func main() {
	handlers.HandleRequests()
}

main関数では上記の通り、handlers パッケージが呼ばれている。

// handlers/handlers.go
package handlers

import (
	"GiveAndTakeCollection-Backend/src/handlers/auth"
	"log"
	"net/http"
)

func HandleRequests() {
	http.HandleFunc("/auth/new", auth.CreateUser)
	http.HandleFunc("/auth/delete", auth.DeleteUser)
	log.Fatal(http.ListenAndServe(":8081", nil))
}

HandleRequests関数は、HTTP リクエストを処理するためのルーティングとサーバーの起動を行っている。

これにより、/auth/newへのリクエストはauth.CreateUser関数で処理され、/auth/deleteへのリクエストはauth.DeleteUser関数で処理されるようになる。

auth.CreateUser が呼ばれた時

auth.CreateUserは下記のようなコード構成になっている。

package auth

import (
	"GiveAndTakeCollection-Backend/src/db"
	"GiveAndTakeCollection-Backend/src/models"
	"encoding/json"
	"fmt"
	"net/http"

	"golang.org/x/crypto/bcrypt"
)

func CreateUser(w http.ResponseWriter, r *http.Request) {
	db := db.DB
	tx := db.Begin() // トランザクションを開始

	// リクエストからデータを取得
	var userData models.User
	err := json.NewDecoder(r.Body).Decode(&userData)
	if err != nil {
		fmt.Println("JSONデコードエラー:", err)
		http.Error(w, "リクエストデータの解析に失敗しました", http.StatusBadRequest)
		return
	}

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(userData.Password), bcrypt.DefaultCost)

	if err != nil {
		fmt.Printf("パスワードのハッシュ化に失敗しました: %s", err)
		http.Error(w, "パスワードのハッシュ化に失敗しました", http.StatusInternalServerError)
		return
	}

	userData.Password = string(hashedPassword)

	// データベースに新しいユーザーを作成
	err = db.Create(&userData).Error
	if err != nil {
		fmt.Println("データベースエラー:", err)
		tx.Rollback()
		http.Error(w, "ユーザーの作成に失敗しました", http.StatusInternalServerError)
		return
	}

	// トランザクションをコミットして確定
	tx.Commit()

	// レスポンスを返す
	w.WriteHeader(http.StatusCreated)
	fmt.Fprintf(w, "ユーザーが作成されました")
}

CreateUser関数を追いかけると、まずdb := db.DBで db パッケージで定義したデータベース接続を取得する(var DB *gorm.DBパッケージレベルで行っていたのはこのような場合のため)。

次にtx := db.Begin()でトランザクションを開始する。

トランザクションというのは、一貫性のないデータを作らないための処理のこと。

var userData models.User
	err := json.NewDecoder(r.Body).Decode(&userData)
	if err != nil {
		fmt.Println("JSONデコードエラー:", err)
		http.Error(w, "リクエストデータの解析に失敗しました", http.StatusBadRequest)
		return
	}

次のブロックはまず、userDataという変数を作成する。 models.Userは型指定。

err := json.NewDecoder(r.Body).Decode(&userData)では、json.NewDecoder(r.Body)で HTTP リクエストのボディを読み込むためのデコーダー(変換機)を作成し、.Decode(&userData)でデコーダーを使ってボディの内容をuserDataという変数に変換(デコード)する。この操作は、JSON 形式のデータを Go の構造体(models.User)に変換する役割を果たしている。

もしエラーが出た際はエラーメッセージを標準出力し、HTTP レスポンスを生成してクライアントにエラーメッセージを返し、処理を終了させている。

エラーがない場合は、この時点でユーザーの登録したいデータ(HTTP リクエストのボディ)がuserDataに格納されている。

その次にuserDataのパスワード(ユーザーの入力したパスワード)をハッシュ化する処理が行われる。

hashedPassword, err := bcrypt.GenerateFromPassword([]byte(userData.Password), bcrypt.DefaultCost)

if err != nil {
	fmt.Printf("パスワードのハッシュ化に失敗しました: %s", err)
	http.Error(w, "パスワードのハッシュ化に失敗しました", http.StatusInternalServerError)
	return
}

// データベースに新しいユーザーを作成
userData.Password = string(hashedPassword)

これはbcryptGoパッケージを使用している。

問題なく、ハッシュ化が行われれば、データベースにユーザーを作成する。

	err = db.Create(&userData).Error
	if err != nil {
		fmt.Println("データベースエラー:", err)
		tx.Rollback()
		http.Error(w, "ユーザーの作成に失敗しました", http.StatusInternalServerError)
		return
	}

	// トランザクションをコミットして確定
	tx.Commit()

	// レスポンスを返す
	w.WriteHeader(http.StatusCreated)
	fmt.Fprintf(w, "ユーザーが作成されました")

err = db.Create(&userData).Errorでデータベースに新規データを作成する。

その際エラーが出たらerrオブジェクトに代入する。

エラーがなければトランザクションをコミットし確定。

成功のレスポンスを返し、ユーザー作成が完了した。

ユーザー作成が出来たので、退会処理も同様に作成する。

auth.DeleteUser が呼ばれた時

package auth

import (
	"GiveAndTakeCollection-Backend/src/db"
	"GiveAndTakeCollection-Backend/src/models"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"

	"gorm.io/gorm"
)

func DeleteUser(w http.ResponseWriter, r *http.Request) {
	db := db.DB
	tx := db.Begin() // トランザクションを開始

	// requestPayloadという構造体を作成する
	var requestPayload struct {
		// この構造体には UserID という名前のフィールドがあり、JSONのキーが user_id と対応している
		UserID uint `json:"user_id"`
	}
	// HTTPリクエストのボディをrequestPayloadにデコードする。結果、{"user_id": 123}のようになる。
	err := json.NewDecoder(r.Body).Decode(&requestPayload)
	if err != nil {
		log.Printf("JSONデコードエラー: %v", err)
		http.Error(w, "リクエストデータの解析に失敗しました", http.StatusBadRequest)
		return
	}

	var user models.User
	// `db.First` メソッドを使用して、データベースから最初に見つかったユーザーレコードを取得
	// &user は、ユーザーデータを取得するための構造体へのポインタ、データベースから取得した情報がこの構造体に格納される
	if err := db.First(&user, requestPayload.UserID).Error; err != nil {
		log.Printf("ユーザーが見つかりません: %v", err)
		tx.Rollback()
		http.Error(w, "指定されたユーザーが見つかりません", http.StatusNotFound)
		return
	}

	// deleted_at フィールドに現在の日時をセット
	user.DeletedAt = gorm.DeletedAt{Time: time.Now()}

	// データベースを更新
	if err := db.Delete(&user).Error; err != nil {
		log.Printf("データベースエラー: %v", err)
		tx.Rollback()
		http.Error(w, "ユーザーの退会に失敗しました", http.StatusInternalServerError)
		return
	}

	// トランザクションをコミットして確定
	if err := tx.Commit().Error; err != nil {
		log.Printf("トランザクションのコミットエラー: %v", err)
		http.Error(w, "ユーザーの退会に失敗しました", http.StatusInternalServerError)
		return
	}

	// ログを使って重要なイベントを残す
	log.Printf("ユーザーが退会しました。ユーザーID: %d", requestPayload.UserID)

	// レスポンスを返す
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "ユーザーが退会しました")
}

これで退会処理もできた。