いたるまで
- Go Lang で WebAPI を作成するために、まずは Docker で MySQL を構築する
- Go Lang で WebAPI を作成するために、Golang で MySQL に接続する
- Go Lang で WebAPI を作成するために、Golang でサーバーを立ち上げる
- Go Lang で WebAPI を作成するために、Golang でエンドポイントにアクセスし DB からデータを取得する
- 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)
これはbcrypt
のGo
パッケージを使用している。
問題なく、ハッシュ化が行われれば、データベースにユーザーを作成する。
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, "ユーザーが退会しました")
}
これで退会処理もできた。