🐻

【速習シリーズ ①】Go言語でREST APIを作ろう!!

2023/12/09に公開

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のインストール

terminal
go get -u github.com/gin-gonic/gin

次にGormのインストールです。
Gormを使用するには、対応するデータベースドライバもインストールする必要があります。例えば、PostgreSQLを使用する場合は、以下のコマンドでPostgreSQLドライバをインストールします。

terminal
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のコンテナを作成します。

docker-compose.yml
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の解説は省略させていただきます。🙏

terminal
docker compose up -d

上記のコマンドを実行するとコンテナが立ち上がります。
ちなみに、今回はdbdiagramを使用して作成しました。

https://dbdiagram.io/d/65731a5a56d8064ca0a77132

3.4 REST APIの構築

3.4.1 Hello Worldを作成

何はともあれ、まずはGinでHello WorldのAPIを作成しましょう!

main.go
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")
}
terminal
go run main.go
json
{
  "message": "Hello World"
}

このコードは、特定のHTTPエンドポイントに対するリクエストを処理し、JSON形式のレスポンスを返します。

  1. Ginエンジンのインスタンスを作成:

    r := gin.Default()
    

    ここで、gin.Default() はGinフレームワークの新しいインスタンスを作成します。このインスタンスは、ルーティング、ミドルウェア、およびリクエスト処理の機能を提供します。

  2. ルート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"} という内容が含まれています。

  3. サーバーの起動:

    r.Run(":8080")
    

    Run メソッドは、指定されたポート(この場合は8080)でWebサーバーを起動します。このサーバーは、上で定義したルートに対するリクエストを待ち受けます。

3.4.2 Gormと接続しよう!

Gormと接続していきます。main関数の中に以下のコードを書いてください。

main.go
// 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)
}

このままでも良いですが、接続情報は秘密にしたいので、環境変数を取得しましょう!
以下のように変更してください。

.env
POSTGRES_PASSWORD=password
POSTGRES_USER=postgres
POSTGRES_DB=postgres
main.go
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関数の外に書いてください。

main.go
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{}を書きます。

main.go
db.AutoMigrate(&Task{})

3.4.3 エンドポイントを作成しよう!

続いて、CRUD操作のAPIを作成していきます。
まずは、Taskを取得するエンドポイントです。
main.goのmain.goのr := gin.Default()の下に書いてください。

main.go
// タスクを取得するエンドポイント
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の下に記述してください。

main.go
// 新しいタスクを作成するエンドポイント
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の下に記述してください。

main.go
// タスクを更新するエンドポイント
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の下に記述してください。

main.go
// タスクを削除するエンドポイント
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を使用してテストを行いました。
https://www.thunderclient.com/

  • POST

  • GET

  • PUT

  • DELETE

4. まとめ

この記事では、Go言語、Gin、Gorm、およびPostgreSQLを使用して簡単なREST APIを構築する方法を学びました。
これらの技術を組み合わせることで、効率的でスケーラブルなWebアプリケーションを構築することができます。

今回のソースコードは以下になります!
https://github.com/ISAWASHUN/gin-todo

5. 次回予告

次回のエピソードでは、今回構築したエンドポイントをさらに洗練させ、アプリケーションの構造を一段と強化します。具体的には、構造体を専用のmodelディレクトリに整理し、レイヤードアーキテクチャの原則に基づいた設計へと進化させていきます。これにより、よりモジュラーで、メンテナンスしやすく、拡張性の高いアプリケーションを目指します。
次回もお楽しみに!

6. 参考文献

https://gorm.io/ja_JP/docs/index.html
https://dbdiagram.io/d/65731a5a56d8064ca0a77132
https://www.thunderclient.com/

Discussion