Docker 環境と Go Gin で最小 API を実装する:MongoDB
こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 24.04 で Go Gin Web アプリケーションを作成し、最小限の REST API を実装する方法を紹介します。
目的
Windows 11 の Linux でクラウド開発します。
こちらから記事の一覧がご覧いただけます。
実現すること
ローカル環境の Ubuntu の Docker 環境で、Dockerfile からビルドした Go Gin サービスのカスタムコンテナーと MongoDB データベースコンテナーを起動します。
ネイティブイメージ形式のアプリをコンテナーとして起動
実行環境
要素 | 概要 |
---|---|
terminal | ターミナル |
Ubuntu | OS |
Docker | コンテナー実行環境 |
API サービス コンテナー
要素 | 概要 |
---|---|
api-todo-gin | API サービス カスタムコンテナー |
app | ネイティブイメージ アプリケーション |
gin | Web サーバー機能を含む |
データベース コンテナー
要素 | 概要 |
---|---|
mongodb-todo | データベースコンテナー |
mongodb | DB サーバー |
db_todo | データベース |
技術トピック
Gin とは?
こちらを展開してご覧いただけます。
Gin (ジン)
Gin は、Go 言語向けの軽量で高速な Web フレームワークです。
キーワード | 内容 |
---|---|
高速性 | Gin は非常に高速で軽量なフレームワークであり、HTTP ルーティングやハンドリングの性能が優れています。これにより、高負荷なアプリケーションでも高い処理能力を発揮します。 |
最小構成 | Gin は最小限の機能を提供するため、不要な機能やコードが含まれません。これにより、アプリケーションのサイズや起動時間を最小限に抑えることができます。 |
パフォーマンス重視 | Gin はルーティングやミドルウェアの処理において、最適化されたアプローチを採用しています。これにより、アプリケーション全体のパフォーマンスが向上します。 |
豊富なミドルウェア | Gin はミドルウェアのサポートを提供し、リクエスト/レスポンスの前後で機能を拡張できます。認証、ロギング、エラーハンドリングなど、多くのミドルウェアが用意されています。 |
学習しやすさ | シンプルなルーティングとクリーンなコード構造により、新しい開発者が迅速に学び始め、プロジェクトに参加することが容易です。 |
活発なコミュニティ | Gin は人気があり、アクティブなコミュニティが存在します。ドキュメントやリソースが豊富で、問題を解決するためのサポートが得られます。 |
開発環境
- Windows 11 Home 23H2 を使用しています。
WSL の Ubuntu を操作しますので macOS の方も参考にして頂けます。
WSL (Microsoft Store アプリ版) ※ こちらの関連記事からインストール方法をご確認いただけます
> wsl --version
WSL バージョン: 2.2.4.0
カーネル バージョン: 5.15.153.1-2
WSLg バージョン: 1.0.61
Ubuntu ※ こちらの関連記事からインストール方法をご確認いただけます
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 24.04 LTS
Release: 24.04
Codename: noble
Docker ※ こちらの関連記事からインストール方法をご確認いただけます
$ docker --version
Docker version 27.0.3, build 7d4bcd8
この記事では基本的に Ubuntu のターミナルで操作を行います。Vim を使用してコピペする方法をはじめて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
作成する REST API の仕様
エンドポイント | HTTPメソッド | 説明 | リクエストBody | レスポンスBody |
---|---|---|---|---|
/todos | GET | すべての ToDo アイテムを取得します。 | None | ToDo アイテムの配列 |
/todos/complete | GET | 完了した ToDo アイテムを取得します。 | None | ToDo アイテムの配列 |
/todos/{id} | GET | ID で ToDo アイテムを取得します。 | None | ToDo アイテム |
/todos | POST | 新しい ToDo アイテムを追加します。 | ToDo アイテム | ToDo アイテム |
/todos/{id} | PUT | 既存の ToDo アイテムを更新します。 | ToDo アイテム | None |
/todos/{id} | DELETE | ID で ToDo アイテムを削除します。 | None | None |
データベース コンテナーの起動
こちらの記事で、ToDo アプリ用の NoSQL データベースを作成し、Docker コンテナーとして起動する手順をご確認いただけます。
データベース コンテナーが起動していることを確認します。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6b411c62bc95 mongo-base "docker-entrypoint.s…" 41 minutes ago Up 41 minutes 0.0.0.0:27017->27017/tcp, :::27017->27017/tcp mongodb-todo
コンテナー間通信するために net-todo という Docker ネットワークをあらかじめ作成しています。ご注意ください。
REST API を実装する手順
Go 言語のインストール
Go 言語をインストールします。
$ sudo apt update
$ sudo apt install golang
Go 言語をアンインストール場合には以下のコマンドを実行します。
$ sudo apt remove --purge golang-go golang-*
$ sudo apt autoremove --purge
$ sudo apt clean
バージョンを確認します。
$ go version
go version go1.22.2 linux/amd64
プロジェクトの作成
プロジェクトフォルダーを作成します。
※ ~/tmp/restapi-gin をプロジェクトフォルダーとします。
$ mkdir -p ~/tmp/restapi-gin
$ cd ~/tmp/restapi-gin
モデルファイルの作成
このシリーズの記事で、RDBMS と NoSQL の両方で同じ API 操作を行います。そのため、ID フィールドは数値型ではなく文字列として定義しています。統一性を保ちつつ異なるデータストアで動作することを目指しています。ご了承ください。
todo.go ファイルを作成します。
$ mkdir -p model
$ vim model/todo.go
package model
import (
"encoding/json"
"log"
"os"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// データベース接続に接続する関数
func ConnectDb() (*mongo.Collection, error) {
var op *options.ClientOptions = options.Client().ApplyURI("mongodb://" +
os.Getenv("DB_USER") + ":" +
os.Getenv("DB_PASSWORD") + "@" +
os.Getenv("DB_HOST") + ":" +
os.Getenv("DB_PORT"),
)
cli, err := mongo.Connect(nil, op) // *mongo.Client, error
if err != nil {
log.Fatal(err)
return nil, err
}
var co *mongo.Collection = cli.Database(os.Getenv("DB_NAME")).Collection("todos")
return co, nil
}
// ToDo エンティティを表す構造体
type Todo struct {
Id primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
Content string `json:"content,omitempty" bson:"content,omitempty"`
CreatedDate time.Time `json:"created_date,omitempty" bson:"created_date,omitempty"`
CompletedDate *time.Time `json:"completed_date,omitempty" bson:"completed_date,omitempty"`
}
// JSON マーシャリング時に呼ばれる完了日に null を挿入するためのメソッド
func (t *Todo) MarshalJSON() ([]byte, error) {
type Alias Todo
if t.CompletedDate == nil {
return json.Marshal(&struct {
*Alias
CompletedDate *time.Time `json:"completed_date"`
}{
Alias: (*Alias)(t),
CompletedDate: nil,
})
}
return json.Marshal((*Alias)(t))
}
// ドキュメントID を "_id" に変換するメソッド
func (t *Todo) ToMongo() bson.M {
return bson.M{
"_id": t.Id,
"content": t.Content,
"created_date": t.CreatedDate,
"completed_date": nil,
}
}
// ドキュメントID を "id" に変換するメソッド
func (t *Todo) FromMongo() bson.M {
return bson.M{
"id": t.Id,
"content": t.Content,
"created_date": t.CreatedDate,
"completed_date": nil,
}
}
アプリケーションファイルの作成
main.go ファイルを作成します。
$ vim main.go
ファイルの内容
package main
import (
"errors"
"log"
"os"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"api-todo/model"
)
func main() {
// gin のデフォルトエンジンを作成
var r *gin.Engine = gin.Default()
// データベース接続・コレクション取得
co, err := model.ConnectDb() // *mongo.Collection, error
if err != nil {
log.Fatal(err)
}
// ミドルウェアにコレクションを設定
r.Use(setCollection("todos", co))
// CORS 設定:適切に修正してください。
var conf cors.Config = cors.DefaultConfig()
conf.AllowAllOrigins = true
conf.AllowCredentials = true
conf.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"}
conf.AllowHeaders = []string{"Origin", "Authorization", "Content-Type"}
r.Use(cors.New(conf))
// ToDo アプリケーションの API エンドポイント定義
r.GET("/todos", getAllTodos)
r.GET("/todos/complete", getCompletedTodos)
r.GET("/todos/:id", getTodo)
r.POST("/todos", createTodo)
r.PUT("/todos/:id", updateTodo)
r.DELETE("/todos/:id", deleteTodo)
// API_PORT 環境変数からポート番号を取得
var port string = os.Getenv("API_PORT")
if port == "" {
port = "5000"
}
// アプリケーションを指定されたポートで起動
err = r.Run(":" + port)
if err != nil {
log.Fatal(err)
}
}
// すべての ToDo アイテムを取得します。
func getAllTodos(ctx *gin.Context) {
var todos []model.Todo
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
cursor, err := co.Find(nil, bson.M{}) // *mongo.Cursor, error
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
defer cursor.Close(nil)
for cursor.Next(nil) {
var todo model.Todo
err = cursor.Decode(&todo)
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
todos = append(todos, todo)
}
ctx.JSON(200, todos)
}
// 完了した ToDo アイテムを取得します。
func getCompletedTodos(ctx *gin.Context) {
var filter primitive.M = bson.M{"completed_date": bson.M{"$ne": nil}}
var todos []model.Todo
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
cursor, err := co.Find(nil, filter) // *mongo.Cursor, error
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
defer cursor.Close(nil)
for cursor.Next(nil) {
var todo model.Todo
err = cursor.Decode(&todo)
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
todos = append(todos, todo)
}
ctx.JSON(200, todos)
}
// ID で ToDo アイテムを取得します。
func getTodo(ctx *gin.Context) {
var id string = ctx.Param("id")
var todo model.Todo
objId, err := primitive.ObjectIDFromHex(id) // primitive.ObjectID, error
if handleError(ctx, 400, "Invalid ID", err) {
return
}
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
err = co.FindOne(nil, bson.M{"_id": objId}).Decode(&todo)
if handleError(ctx, 404, "Todo not found", err) {
return
}
ctx.JSON(200, todo)
}
// 新しい ToDo アイテムを追加します。
func createTodo(ctx *gin.Context) {
var create model.Todo
err := ctx.BindJSON(&create)
if handleError(ctx, 400, "Invalid input", err) {
return
}
create.Id = primitive.NewObjectID()
create.CreatedDate = time.Now()
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
_, err = co.InsertOne(nil, create.ToMongo())
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
ctx.JSON(201, create.FromMongo())
}
// 既存の ToDo アイテムを更新します。
func updateTodo(ctx *gin.Context) {
var id string = ctx.Param("id")
objId, err := primitive.ObjectIDFromHex(id) // primitive.ObjectID, error
if handleError(ctx, 400, "Invalid ID", err) {
return
}
var update model.Todo
err = ctx.BindJSON(&update)
if handleError(ctx, 400, "Invalid input", err) {
return
}
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
var exist model.Todo
err = co.FindOne(nil, bson.M{"_id": objId}).Decode(&exist)
if handleError(ctx, 404, "Todo not found", err) {
return
}
update.Id = exist.Id
update.CreatedDate = exist.CreatedDate
_, err = co.ReplaceOne(nil, bson.M{"_id": objId}, update)
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
ctx.JSON(200, update)
}
// ID で ToDo アイテムを削除します。
func deleteTodo(ctx *gin.Context) {
var id string = ctx.Param("id")
objId, err := primitive.ObjectIDFromHex(id) // primitive.ObjectID, error
if handleError(ctx, 400, "Invalid ID", err) {
return
}
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
_, err = co.DeleteOne(nil, bson.M{"_id": objId})
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
ctx.JSON(204, nil)
}
// gin のコンテキストにコレクションを保持する関数
func setCollection(name string, co *mongo.Collection) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set(name, co)
c.Next()
}
}
// gin のコンテキストにコレクションを取得する関数
func getCollection(name string, ctx *gin.Context) (*mongo.Collection, bool) {
value, exists := ctx.Get(name) // any, bool
if !exists {
handleError(ctx, 500, "Internal Server Error", errors.New("Collection not found in context."))
return nil, false
}
co, ok := value.(*mongo.Collection) // *mongo.Collection, bool
if !ok {
handleError(ctx, 500, "Internal Server Error", errors.New("Failed to cast collection."))
return nil, false
}
return co, true
}
// カスタムのエラーハンドリング関数
func handleError(ctx *gin.Context, code int, msg string, err error) bool {
if err != nil {
log.Println(msg + ": " + err.Error())
ctx.JSON(code, gin.H{"error": "An internal server error occurred."})
return true
}
return false
}
パッケージの追加
プロジェクトを初期化します。
$ go mod init api-todo
パッケージをインストールします。
$ go get -u \
github.com/gin-gonic/gin \
github.com/gin-contrib/cors \
go.mongodb.org/mongo-driver/bson \
go.mongodb.org/mongo-driver/bson/primitive \
go.mongodb.org/mongo-driver/mongo \
go.mongodb.org/mongo-driver/mongo/options
バージョンを確認します。
$ go list -m github.com/gin-gonic/gin
github.com/gin-gonic/gin v1.10.0
$ go list -m github.com/gin-contrib/cors
github.com/gin-contrib/cors v1.7.2
$ go list -m go.mongodb.org/mongo-driver
go.mongodb.org/mongo-driver v1.16.1
アプリのビルドと起動
環境変数を作成します。
export DB_HOST=localhost
export DB_PORT=27017
export DB_NAME=db_todo
export DB_USER=root
export DB_PASSWORD=password
この環境変数が一時的なものであることに注意してください。
アプリを起動します。
※ アプリを停止するときは ctrl + C を押します。
$ export API_PORT=5000 && go run main.go
ここまでの手順で、Ubuntu でアプリを起動することができました。
アプリの動作確認
別ターミナルから curl コマンドで確認します。
必要な場合、jq をインストールします。
$ sudo apt update
$ sudo apt install jq
GET: /todos エンドポイントの動作確認
すべての ToDo アイテムを取得します。
$ curl -s http://localhost:5000/todos | jq '.'
レスポンス
[
// 省略
{
"id": "66b7f3866c2fb3b3a7149f4a",
"content": "運動する",
"created_date": "2024-08-10T23:11:02.524Z",
"completed_date": "2024-08-10T23:11:02.524Z"
},
{
"id": "66b7f3866c2fb3b3a7149f4b",
"content": "本を読む",
"created_date": "2024-08-10T23:11:02.524Z",
"completed_date": null
},
{
"id": "66b7f3866c2fb3b3a7149f4c",
"content": "請求書を支払う",
"created_date": "2024-08-10T23:11:02.524Z",
"completed_date": null
},
// 省略
すべての ToDo アイテムを取得できています。
GET: /todos/complete エンドポイントの動作確認
完了した ToDo アイテムを取得します。
$ curl -s http://localhost:5000/todos/complete | jq '.'
レスポンス
[
{
"id": "66b7f3866c2fb3b3a7149f48",
"content": "食材を買う",
"created_date": "2024-08-10T23:11:02.524Z",
"completed_date": "2024-08-10T23:11:02.524Z"
},
{
"id": "66b7f3866c2fb3b3a7149f49",
"content": "報告書を仕上げる",
"created_date": "2024-08-10T23:11:02.524Z",
"completed_date": "2024-08-10T23:11:02.524Z"
},
{
"id": "66b7f3866c2fb3b3a7149f4a",
"content": "運動する",
"created_date": "2024-08-10T23:11:02.524Z",
"completed_date": "2024-08-10T23:11:02.524Z"
}
]
完了した ToDo アイテムを取得できています。
GET: /todos/{id} エンドポイントの動作確認
ID で ToDo アイテムを取得します。
id: 66b7f3866c2fb3b3a7149f4f は、その都度適切な値を設定してください。
$ curl -s http://localhost:5000/todos/66b7f3866c2fb3b3a7149f4f | jq '.'
レスポンス
{
"id": "66b7f3866c2fb3b3a7149f4f",
"content": "コードを書く",
"created_date": "2024-08-10T23:11:02.524Z"
}
ID で ToDo アイテムを取得できています。
POST: /todos エンドポイントの動作確認
新しい ToDo アイテムを追加します。
$ curl -s -X POST http://localhost:5000/todos \
-H 'Content-Type: application/json; charset=utf-8' \
-d \
'{
"content": "昼寝をする"
}' | jq '.'
レスポンス
{
"completed_date": null,
"content": "昼寝をする",
"created_date": "2024-08-11T09:56:51.377707042+09:00",
"id": "66b80c5361708660bc0490fd"
}
新しい ToDo アイテムが追加できています。
PUT: /todos/{id} エンドポイントの動作確認
既存の ToDo アイテムを更新します。
id: 66b80c5361708660bc0490fd は、その都度適切な値を設定してください。
$ curl -s -X PUT http://localhost:5000/todos/66b80c5361708660bc0490fd \
-H 'Content-Type: application/json; charset=utf-8' \
-d \
'{
"content": "窓を開ける"
}' | jq '.'
レスポンス
{
"id": "66b80c5361708660bc0490fd",
"content": "窓を開ける",
"created_date": "2024-08-11T00:56:51.377Z"
}
ToDo アイテムが更新できています。
DELETE: /todos/{id} エンドポイントの動作確認
ID で ToDo アイテムを削除します。
id: 66b80c5361708660bc0490fd は、その都度適切な値を設定してください。
$ curl -s -X DELETE http://localhost:5000/todos/66b80c5361708660bc0490fd | jq '.'
レスポンス
※なし
レスポンスはありませんが、DB のコレクションを確認すると対象のレコードが削除されています。
ここまでの手順で、最小限の CRUD 操作を行う REST API をアプリに実装できました。
コンテナーイメージの作成
Dockerfile を作成します。
$ vim Dockerfile
ファイルの内容
# build the app.
FROM golang:1.22 AS build-env
# set the working dir.
WORKDIR /app
# copy the go module dependency files.
COPY go.mod go.sum ./
# download the go module dependencies.
RUN go mod download
# copy the app source code.
COPY . .
# build the go app binary.
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app main.go
# set up the container.
FROM debian:12-slim
# set the working dir.
WORKDIR /app
# copy the built app binary from the build-env.
COPY --from=build-env /app/app ./app
# expose the port.
EXPOSE 5000
# command to run the app.
CMD ["./app"]
Docker デーモンを起動します。
$ sudo service docker start
Docker 環境をお持ちでない場合は、以下の関連記事から Docker Engine のインストール手順をご確認いただけます。
コンテナーイメージをビルドします。
$ docker build \
--no-cache \
--tag api-todo-gin:latest .
コンテナーイメージを確認します。
$ docker images | grep api-todo-gin
api-todo-gin latest 077f0bad8f71 17 seconds ago 91.5MB
ここまでの手順で、ローカル環境の Docker にアプリのカスタムコンテナーイメージをビルドすることができました。
コンテナーを起動
ローカルでコンテナーを起動します。
※ コンテナーを停止するときは ctrl + C を押します。
コンテナー間通信するために net-todo という Docker ネットワークをあらかじめ作成しています。ご注意ください。
$ docker run --rm \
--publish 5000:5000 \
--name api-local \
--net net-todo \
--env DB_HOST=mongodb-todo \
--env DB_PORT=27017 \
--env DB_NAME=db_todo \
--env DB_USER=root \
--env DB_PASSWORD=password \
api-todo-gin
ここまでの手順で、ローカル環境の Docker でアプリのカスタムコンテナーを起動することができました。
コンテナーの動作確認
別ターミナルから curl コマンドで確認します。
※ ID で ToDo アイテムを取得します。
id: 66b7f3866c2fb3b3a7149f4f は、その都度適切な値を設定してください。
$ curl -s http://localhost:5000/todos/66b7f3866c2fb3b3a7149f4f | jq '.'
レスポンス
{
"id": "66b7f3866c2fb3b3a7149f4f",
"content": "コードを書く",
"created_date": "2024-08-10T23:11:02.524Z"
}
ここまでの手順で、ターミナルにレスポンスが表示され、JSON データを取得することができました。
コンテナーの状態を確認してみます。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
50f487b8daf8 api-todo-gin "./app" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp, :::5000->5000/tcp api-local
6b411c62bc95 mongo-base "docker-entrypoint.s…" 3 hours ago Up 3 hours 0.0.0.0:27017->27017/tcp, :::27017->27017/tcp mongodb-todo
コンテナーに接続
別ターミナルからコンテナーに接続します。
$ docker exec -it api-local /bin/bash
コンテナー接続後にディレクトリを確認します。
※ コンテナーから出るときは ctrl + D を押します。
# pwd
/app
# ls -lah
total 16M
drwxr-xr-x 1 root root 4.0K Aug 11 01:50 .
drwxr-xr-x 1 root root 4.0K Aug 11 02:00 ..
-rwxr-xr-x 1 root root 16M Aug 11 01:50 app
top コマンドで状況を確認します。
# apt update
# apt install procps
# top
top - 02:04:08 up 3:01, 0 user, load average: 0.00, 0.04, 0.07
Tasks: 3 total, 1 running, 2 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 15949.2 total, 14549.9 free, 1145.5 used, 563.5 buff/cache
MiB Swap: 4096.0 total, 4096.0 free, 0.0 used. 14803.7 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 1236044 11200 8308 S 0.3 0.1 0:00.08 app
12 root 20 0 4188 3480 2972 S 0.0 0.0 0:00.02 bash
203 root 20 0 8560 4568 2700 R 0.0 0.0 0:00.00 top
コンテナーの情報を表示してみます。
# cat /etc/*-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
このコンテナーは Debian GNU/Linux をベースに作成されています。つまり、Debian GNU/Linux と同じように扱うことができます。
おまけ:Swagger 装備
ライブラリの追加
swag コマンドをインストールします。
$ go install github.com/swaggo/swag/cmd/swag@latest
$ export PATH=$PATH:/home/$USER/go/bin
gin-swagger パッケージをインストールします。
$ go get -u github.com/swaggo/gin-swagger
$ go get -u github.com/swaggo/files
コードの修正
コードに以下の内容を追記します。
import (
// 省略
"api-todo/docs"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
// 省略
)
func main() {
// 省略
// Swagger の設定
docs.SwaggerInfo.Title = "Todo API"
docs.SwaggerInfo.Version = "0.1.0"
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
// 省略
}
// @Summary Get All Todos
// @Description すべての ToDo アイテムを取得します。
// @Tags todos
// @Accept json
// @Produce json
// @Success 200 {array} model.Todo
// @Router /todos [get]
func getAllTodos(ctx *gin.Context) {
// 省略
}
// @Summary Get Completed Todos
// @Description 完了した ToDo アイテムを取得します。
// @Tags todos
// @Accept json
// @Produce json
// @Success 200 {array} model.Todo
// @Router /todos/complete [get]
func getCompletedTodos(ctx *gin.Context) {
// 省略
}
// @Summary Get Todo
// @Description ID で ToDo アイテムを取得します。
// @Tags todos
// @Accept json
// @Produce json
// @Param id path string true "Todo ID"
// @Success 200 {object} model.Todo
// @Router /todos/{id} [get]
func getTodo(ctx *gin.Context) {
// 省略
}
// @Summary Create Todo
// @Description 新しい ToDo アイテムを追加します。
// @Tags todos
// @Accept json
// @Produce json
// @Param todo body model.Todo true "Todo to create"
// @Success 201 {object} model.Todo
// @Router /todos [post]
func createTodo(ctx *gin.Context) {
// 省略
}
// @Summary Update Todo
// @Description ToDo アイテムを更新します。
// @Tags todos
// @Accept json
// @Produce json
// @Param id path string true "Todo ID"
// @Param todo body model.Todo true "Todo to update"
// @Success 200 {object} model.Todo
// @Router /todos/{id} [put]
func updateTodo(ctx *gin.Context) {
// 省略
}
// @Summary Delete Todo
// @Description ID で ToDo アイテムを削除します。
// @Tags todos
// @Accept json
// @Produce json
// @Param id path string true "Todo ID"
// @Success 204 {object} nil
// @Router /todos/{id} [delete]
func deleteTodo(ctx *gin.Context) {
// 省略
}
コードの全体を表示する
package main
import (
"errors"
"log"
"os"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"api-todo/model"
"api-todo/docs"
)
func main() {
// gin のデフォルトエンジンを作成
var r *gin.Engine = gin.Default()
// データベース接続・コレクション取得
co, err := model.ConnectDb() // *mongo.Collection, error
if err != nil {
log.Fatal(err)
}
// ミドルウェアにコレクションを設定
r.Use(setCollection("todos", co))
// CORS 設定:適切に修正してください。
var conf cors.Config = cors.DefaultConfig()
conf.AllowAllOrigins = true
conf.AllowCredentials = true
conf.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"}
conf.AllowHeaders = []string{"Origin", "Authorization", "Content-Type"}
r.Use(cors.New(conf))
// ToDo アプリケーションの API エンドポイント定義
r.GET("/todos", getAllTodos)
r.GET("/todos/complete", getCompletedTodos)
r.GET("/todos/:id", getTodo)
r.POST("/todos", createTodo)
r.PUT("/todos/:id", updateTodo)
r.DELETE("/todos/:id", deleteTodo)
// Swagger の設定
docs.SwaggerInfo.Title = "Todo API"
docs.SwaggerInfo.Version = "0.1.0"
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
// API_PORT 環境変数からポート番号を取得
var port string = os.Getenv("API_PORT")
if port == "" {
port = "5000"
}
// アプリケーションを指定されたポートで起動
err = r.Run(":" + port)
if err != nil {
log.Fatal(err)
}
}
// @Summary Get All Todos
// @Description すべての ToDo アイテムを取得します。
// @Tags todos
// @Accept json
// @Produce json
// @Success 200 {array} model.Todo
// @Router /todos [get]
func getAllTodos(ctx *gin.Context) {
var todos []model.Todo
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
cursor, err := co.Find(nil, bson.M{}) // *mongo.Cursor, error
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
defer cursor.Close(nil)
for cursor.Next(nil) {
var todo model.Todo
err = cursor.Decode(&todo)
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
todos = append(todos, todo)
}
ctx.JSON(200, todos)
}
// @Summary Get Completed Todos
// @Description 完了した ToDo アイテムを取得します。
// @Tags todos
// @Accept json
// @Produce json
// @Success 200 {array} model.Todo
// @Router /todos/complete [get]
func getCompletedTodos(ctx *gin.Context) {
var filter primitive.M = bson.M{"completed_date": bson.M{"$ne": nil}}
var todos []model.Todo
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
cursor, err := co.Find(nil, filter) // *mongo.Cursor, error
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
defer cursor.Close(nil)
for cursor.Next(nil) {
var todo model.Todo
err = cursor.Decode(&todo)
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
todos = append(todos, todo)
}
ctx.JSON(200, todos)
}
// @Summary Get Todo
// @Description ID で ToDo アイテムを取得します。
// @Tags todos
// @Accept json
// @Produce json
// @Param id path string true "Todo ID"
// @Success 200 {object} model.Todo
// @Router /todos/{id} [get]
func getTodo(ctx *gin.Context) {
var id string = ctx.Param("id")
var todo model.Todo
objId, err := primitive.ObjectIDFromHex(id) // primitive.ObjectID, error
if handleError(ctx, 400, "Invalid ID", err) {
return
}
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
err = co.FindOne(nil, bson.M{"_id": objId}).Decode(&todo)
if handleError(ctx, 404, "Todo not found", err) {
return
}
ctx.JSON(200, todo)
}
// @Summary Create Todo
// @Description 新しい ToDo アイテムを追加します。
// @Tags todos
// @Accept json
// @Produce json
// @Param todo body model.Todo true "Todo to create"
// @Success 201 {object} model.Todo
// @Router /todos [post]
func createTodo(ctx *gin.Context) {
var create model.Todo
err := ctx.BindJSON(&create)
if handleError(ctx, 400, "Invalid input", err) {
return
}
create.Id = primitive.NewObjectID()
create.CreatedDate = time.Now()
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
_, err = co.InsertOne(nil, create.ToMongo())
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
ctx.JSON(201, create.FromMongo())
}
// @Summary Update Todo
// @Description ToDo アイテムを更新します。
// @Tags todos
// @Accept json
// @Produce json
// @Param id path string true "Todo ID"
// @Param todo body model.Todo true "Todo to update"
// @Success 200 {object} model.Todo
// @Router /todos/{id} [put]
func updateTodo(ctx *gin.Context) {
var id string = ctx.Param("id")
objId, err := primitive.ObjectIDFromHex(id) // primitive.ObjectID, error
if handleError(ctx, 400, "Invalid ID", err) {
return
}
var update model.Todo
err = ctx.BindJSON(&update)
if handleError(ctx, 400, "Invalid input", err) {
return
}
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
var exist model.Todo
err = co.FindOne(nil, bson.M{"_id": objId}).Decode(&exist)
if handleError(ctx, 404, "Todo not found", err) {
return
}
update.Id = exist.Id
update.CreatedDate = exist.CreatedDate
_, err = co.ReplaceOne(nil, bson.M{"_id": objId}, update)
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
ctx.JSON(200, update)
}
// @Summary Delete Todo
// @Description ID で ToDo アイテムを削除します。
// @Tags todos
// @Accept json
// @Produce json
// @Param id path string true "Todo ID"
// @Success 204 {object} nil
// @Router /todos/{id} [delete]
func deleteTodo(ctx *gin.Context) {
var id string = ctx.Param("id")
objId, err := primitive.ObjectIDFromHex(id) // primitive.ObjectID, error
if handleError(ctx, 400, "Invalid ID", err) {
return
}
co, ok := getCollection("todos", ctx) // *mongo.Collection, bool
if !ok {
return
}
_, err = co.DeleteOne(nil, bson.M{"_id": objId})
if handleError(ctx, 500, "Internal Server Error", err) {
return
}
ctx.JSON(204, nil)
}
// gin のコンテキストにコレクションを保持する関数
func setCollection(name string, co *mongo.Collection) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set(name, co)
c.Next()
}
}
// gin のコンテキストにコレクションを取得する関数
func getCollection(name string, ctx *gin.Context) (*mongo.Collection, bool) {
value, exists := ctx.Get(name) // any, bool
if !exists {
handleError(ctx, 500, "Internal Server Error", errors.New("Collection not found in context."))
return nil, false
}
co, ok := value.(*mongo.Collection) // *mongo.Collection, bool
if !ok {
handleError(ctx, 500, "Internal Server Error", errors.New("Failed to cast collection."))
return nil, false
}
return co, true
}
// カスタムのエラーハンドリング関数
func handleError(ctx *gin.Context, code int, msg string, err error) bool {
if err != nil {
log.Println(msg + ": " + err.Error())
ctx.JSON(code, gin.H{"error": "An internal server error occurred."})
return true
}
return false
}
Swag でドキュメントを生成します。
$ swag init
2024/08/11 11:27:51 Generate swagger docs....
2024/08/11 11:27:51 Generate general API Info, search dir:./
2024/08/11 11:27:51 create docs.go at docs/docs.go
2024/08/11 11:27:51 create swagger.json at docs/swagger.json
2024/08/11 11:27:51 create swagger.yaml at docs/swagger.yaml
Swagger を確認
Web ブラウザで Swagger の URL を確認します。
http://localhost:5000/swagger/index.html
これらのツールを活用することにより、より手軽に API を操作できます。
まとめ
WSL Ubuntu の Docker 環境で、Go Gin の最小 API を実装することができました。
この記事の実装例は1つのアプローチに過ぎず、必ずしも正しい方法とは限りません。他にも多様な方法がありますので、さまざまな情報を照らし合わせて検討してみてください。
どうでしたか? WSL Ubuntu で、Go Gin Web アプリケーションを手軽に起動できます。ぜひお試しください。今後も Go の開発環境などを紹介しますので、ぜひお楽しみにしてください。