【速習シリーズ ①】Go言語でREST APIを作ろう!!
1. 本記事の意図
こんにちは、どすこいです!
最近Go言語を勉強し始めまして、output用にこの記事を書きました!
今回のシリーズでは、Go言語を使用して簡単なREST APIを構築する方法を紹介します。
Go言語はそのパフォーマンスの高さと並行処理の容易さで知られており、Webサービスの開発に適しています。
この記事を通じて、Go言語の基本的なWeb開発技術を学び、実際に動作するAPIを構築することができます。
対象者
- GO言語の基礎文法を学んだ方
- 簡単なWeb APIを作ってアウトプットしたい方
2. 使用技術: Go, Gin, Gorm, PostgreSQL
このプロジェクトでは以下の技術を使用します:
- Go言語: 高性能なコンパイル言語で、Webサーバーの開発に適しています。
- Gin: Go言語用の高速なHTTP Webフレームワークで、ルーティングやミドルウェアのサポートを提供します。
- Gorm: Go言語のオブジェクト関係マッピング(ORM)ライブラリで、データベース操作を簡単にします。
- PostgreSQL: オープンソースの関係データベースシステムで、信頼性と拡張性が高いです。
3. 開発
3.1 環境構築
まず、Go言語、PostgreSQL、および必要なライブラリをインストールします。
GinとGormはGoのパッケージマネージャを使用してインストールできます。
Ginのインストール
go get -u github.com/gin-gonic/gin
次にGormのインストールです。
Gormを使用するには、対応するデータベースドライバもインストールする必要があります。例えば、PostgreSQLを使用する場合は、以下のコマンドでPostgreSQLドライバをインストールします。
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
3.2 プロジェクトの構造
.
├── Makefile
├── README.md
├── docker-compose.yml
├── go.mod
├── go.sum
└── main.go
3.3 データベースの設定
まずは、dockerでPosgresqlのコンテナを作成します。
version: '3'
services:
db:
image: postgres:14
container_name: postgres
ports:
- 5432:5432
volumes:
- db-store:/var/lib/postgresql/data
- ./script:/docker-entrypoint-initdb.d
environment:
- POSTGRES_PASSWORD=password
volumes:
db-store:
今回は、Goの解説をしたいので、dockerの解説は省略させていただきます。🙏
docker compose up -d
上記のコマンドを実行するとコンテナが立ち上がります。
ちなみに、今回はdbdiagramを使用して作成しました。
3.4 REST APIの構築
3.4.1 Hello Worldを作成
何はともあれ、まずはGinでHello WorldのAPIを作成しましょう!
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// Ginエンジンのインスタンスを作成
r := gin.Default()
// ルートURL ("/") に対するGETリクエストをハンドル
r.GET("/", func(c *gin.Context) {
// JSONレスポンスを返す
c.JSON(200, gin.H{
"message": "Hello World",
})
})
// 8080ポートでサーバーを起動
r.Run(":8080")
}
go run main.go
{
"message": "Hello World"
}
このコードは、特定のHTTPエンドポイントに対するリクエストを処理し、JSON形式のレスポンスを返します。
-
Ginエンジンのインスタンスを作成:
r := gin.Default()
ここで、
gin.Default()
はGinフレームワークの新しいインスタンスを作成します。このインスタンスは、ルーティング、ミドルウェア、およびリクエスト処理の機能を提供します。 -
ルートURLに対するGETリクエストのハンドリング:
r.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "Hello World", }) })
この部分では、ルートURL(
"/"
)に対するGETリクエストをハンドルするルートを定義しています。リクエストが来ると、無名関数が実行され、ステータスコード200(HTTP OK)と共にJSON形式のレスポンスが返されます。このレスポンスには、{"message": "Hello World"}
という内容が含まれています。 -
サーバーの起動:
r.Run(":8080")
Run
メソッドは、指定されたポート(この場合は8080)でWebサーバーを起動します。このサーバーは、上で定義したルートに対するリクエストを待ち受けます。
3.4.2 Gormと接続しよう!
Gormと接続していきます。main関数の中に以下のコードを書いてください。
// PostgreSQL接続情報
dsn := "host=localhost user=yourusername password=yourpassword dbname=yourdbname port=5432 sslmode=disable TimeZone=Asia/Tokyo"
// GORMでデータベースに接続
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
このままでも良いですが、接続情報は秘密にしたいので、環境変数を取得しましょう!
以下のように変更してください。
POSTGRES_PASSWORD=password
POSTGRES_USER=postgres
POSTGRES_DB=postgres
package main
import (
"fmt"
"log"
"os"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
// .envファイルから環境変数を読み込む
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
// 環境変数から接続情報を取得
dbUser := os.Getenv("POSTGRES_USER")
dbPassword := os.Getenv("POSTGRES_PASSWORD")
dbName := os.Getenv("POSTGRES_DB")
dbHost := "localhost" // または環境変数から取得
dbPort := "5432" // または環境変数から取得
// DSNを構築
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Tokyo", dbHost, dbUser, dbPassword, dbName, dbPort)
// GORMでデータベースに接続
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// データベースにテーブルを作成
db.AutoMigrate()
// Ginエンジンのインスタンスを作成
r := gin.Default()
// ルートURL ("/") に対するGETリクエストをハンドル
r.GET("/", func(c *gin.Context) {
// JSONレスポンスを返す
c.JSON(200, gin.H{
"message": "Hello World",
})
})
// 8080ポートでサーバーを起動
r.Run(":8080")
}
上記のコードの説明です。
.envファイルから環境変数を読み込む:
godotenv.Load() は.envファイルを読み込み、その内容を現在のプロセスの環境変数として設定します。これにより、.envファイル内のキー=値のペアが環境変数として利用可能になります。
os.Getenvは、この関数は指定された環境変数の値を取得します。ここでは、PostgreSQLデータベースのユーザー名、パスワード、データベース名を取得しています。
続いて、Gormのdb.AutoMigrate()について説明します。
db.AutoMigrate() は、GORMを使用したGo言語のコードで見られる一般的な命令です。この命令は、データベースのスキーマを自動的に移行(更新)するために使用されます。
具体的には、以下のような機能を提供します:
テーブルの自動作成: 構造体に基づいて、対応するテーブルがデータベース内に存在しない場合、GORMはそのテーブルを自動的に作成します。
スキーマの自動更新: 既存のテーブルのスキーマ(構造)が Task 構造体の定義と異なる場合、GORMはテーブルのスキーマを自動的に更新します。これには、新しい列の追加や既存の列の型の変更などが含まれます。
安全なスキーマ変更: AutoMigrate は、データの損失を引き起こす可能性のある変更(列の削除やデータ型の変更など)を行いません。そのため、この機能は比較的安全に使用できます。
つまり、構造体を作成し、そこから、DBのスキーマを自動で生成してくれます。
今回は、以下のSQL文をGormの書き方に合うように合うよう書いていきます。
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
task VARCHAR(255), -- タスクの最大長を255文字に設定
is_completed BOOLEAN, -- タスクが完了したかどうか
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- レコード作成時のタイムスタンプ
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -- レコード更新時のタイムスタンプ
);
3.4.3 構造体を書こう!
main.goの中に以下のように書いてください。
ただし、main関数の外に書いてください。
type Task struct {
ID uint `gorm:"primary_key"`
Task string `gorm:"size:255"`
IsCompleted bool `gorm:"default:false"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"`
}
上記のコードの解説をしていきます。
IDフィールド:
uint 型(符号なし整数)で、タスクの一意な識別子として機能します。
gorm:"primary_key" タグは、このフィールドがテーブルの主キーであることを示します。
Taskフィールド:
string 型で、タスクの内容や説明を格納します。
gorm:"size:255" タグは、このフィールドが最大255文字の長さを持つことを示します。
IsCompletedフィールド:
bool 型(真偽値)で、タスクが完了したかどうかを示します。
gorm:"default:false" タグは、新しいレコードが作成されたときにこのフィールドのデフォルト値が false であることを示します。
CreatedAtフィールド:
time.Time 型で、タスクが作成された日時を格納します。
gorm:"default:CURRENT_TIMESTAMP" タグは、新しいレコードが作成されたときに現在のタイムスタンプを自動的にこのフィールドに設定することを示します。
UpdatedAtフィールド:
time.Time 型で、タスクが最後に更新された日時を格納します。
gorm:"default:CURRENT_TIMESTAMP" タグは、レコードが更新されるたびに現在のタイムスタンプを自動的にこのフィールドに設定することを示します。
続いて、先ほどのdb.AutoMigrate()の引数に&Task{}を書きます。
db.AutoMigrate(&Task{})
3.4.3 エンドポイントを作成しよう!
続いて、CRUD操作のAPIを作成していきます。
まずは、Taskを取得するエンドポイントです。
main.goのmain.goのr := gin.Default()の下に書いてください。
// タスクを取得するエンドポイント
r.GET("/tasks", func(c *gin.Context) {
var tasks []Task
db.Find(&tasks)
c.JSON(http.StatusOK, tasks)
})
-
r.GET("/tasks", ...): この行は、URLパスが /tasks であるGETリクエストをハンドルするためのルートを定義しています。クライアントがこのエンドポイントにリクエストを送ると、指定された無名関数が実行されます。
-
var tasks []Task: Task 型のスライスを宣言します。このスライスは、データベースから取得されるタスクを格納するために使用されます。
-
db.Find(&tasks): GORMの Find メソッドを使用して、データベース内の全ての Task レコードを取得し、それらを tasks スライスに格納します。
-
c.JSON(http.StatusOK, tasks): Ginの JSON メソッドを使用して、ステータスコード 200 OK と共に取得したタスクのリストをJSON形式でクライアントに返します。
続いて、TaskをCreateするエンドポイントを作成します。
先ほどのGetの下に記述してください。
// 新しいタスクを作成するエンドポイント
r.POST("/tasks", func(c *gin.Context) {
var task Task
if err := c.ShouldBindJSON(&task); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db.Create(&task)
c.JSON(http.StatusOK, task)
})
ここでは、Gormの機能である、db.Create(&task)を用いて、taskをpostしています。
続いて、TaskをUpdateするエンドポイントを作成します。
先ほどのPostの下に記述してください。
// タスクを更新するエンドポイント
r.PUT("/tasks/:id", func(c *gin.Context) {
var task Task
id := c.Param("id")
if err := db.First(&task, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
}
if err := c.ShouldBindJSON(&task); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db.Save(&task)
c.JSON(http.StatusOK, task)
})
-
var task Task: Task 型の変数を宣言します。この変数は、データベースから取得される特定のタスクを格納するために使用されます。
id := c.Param("id"): リクエストURLからタスクのIDを取得します。
db.First(&task, id): GORMの First メソッドを使用して、指定されたIDを持つ最初の Task レコードを取得し、それを task 変数に格納します。レコードが見つからない場合はエラーを返します。 -
db.Save(&task): GORMの Save メソッドを使用して、更新された task をデータベースに保存します。
c.JSON(http.StatusOK, task): 更新されたタスクをJSON形式でクライアントに返します。
最後に、Deleteのエンドポイントを作成します。
先ほどのUpdateの下に記述してください。
// タスクを削除するエンドポイント
r.DELETE("/tasks/:id", func(c *gin.Context) {
var task Task
id := c.Param("id")
if err := db.First(&task, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
}
db.Delete(&task)
c.JSON(http.StatusOK, gin.H{"message": "Task deleted"})
})
updateと同じように、id := c.Param("id")でidを取得し、Gormの機能であるdb.Delete(&task)で削除しています。
以上でCRUD操作のエンドポイントの作成ができました!!
3.5 テスト
APIの各エンドポイントをテストし、期待通りに動作することを確認します。
今回は、VSCodeの拡張機能であるThunder Clientを使用してテストを行いました。
-
POST
-
GET
-
PUT
-
DELETE
4. まとめ
この記事では、Go言語、Gin、Gorm、およびPostgreSQLを使用して簡単なREST APIを構築する方法を学びました。
これらの技術を組み合わせることで、効率的でスケーラブルなWebアプリケーションを構築することができます。
今回のソースコードは以下になります!
5. 次回予告
次回のエピソードでは、今回構築したエンドポイントをさらに洗練させ、アプリケーションの構造を一段と強化します。具体的には、構造体を専用のmodelディレクトリに整理し、レイヤードアーキテクチャの原則に基づいた設計へと進化させていきます。これにより、よりモジュラーで、メンテナンスしやすく、拡張性の高いアプリケーションを目指します。
次回もお楽しみに!
6. 参考文献
Discussion