0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker 環境と Go Gin で最小 API を実装する:MongoDB

Last updated at Posted at 2024-08-11

Docker 環境と Go Gin で最小 API を実装する:MongoDB

こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 24.04 で Go Gin Web アプリケーションを作成し、最小限の REST API を実装する方法を紹介します。
gin_and_mongodb_on_docker.png

目的

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

モデルファイルの作成

このシリーズの記事で、RDBMSNoSQL の両方で同じ API 操作を行います。そのため、ID フィールドは数値型ではなく文字列として定義しています。統一性を保ちつつ異なるデータストアで動作することを目指しています。ご了承ください。

todo.go ファイルを作成します。

$ mkdir -p model
$ vim model/todo.go
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

ファイルの内容

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

ファイルの内容

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

コードの修正

コードに以下の内容を追記します。

main.go ※一部分
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) {
    // 省略
}
コードの全体を表示する
main.go
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

image.png

これらのツールを活用することにより、より手軽に API を操作できます。

まとめ

WSL Ubuntu の Docker 環境で、Go Gin の最小 API を実装することができました。

この記事の実装例は1つのアプローチに過ぎず、必ずしも正しい方法とは限りません。他にも多様な方法がありますので、さまざまな情報を照らし合わせて検討してみてください。

どうでしたか? WSL Ubuntu で、Go Gin Web アプリケーションを手軽に起動できます。ぜひお試しください。今後も Go の開発環境などを紹介しますので、ぜひお楽しみにしてください。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?