0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

初心者向け!GoでAPIサーバーを作成する手順を徹底解説 ~Docker連携でMySQLを操作する実装~

Posted at

はじめに

APIサーバーを構築する際、Go(Golang)とGinフレームワークを使うのは非常に効率的です。この記事では、コードを細かく分解し、初めての方でもわかりやすいように徹底的に解説します。また、Dockerで立ち上げたMySQLと接続する部分も含めて、環境構築から動作確認までを網羅します。

前提条件

  • Goの基本的な知識(簡単な構文を理解しているレベルでOK)
  • Docker環境のセットアップ済み
  • MySQLをDockerで動かしている

実装するコードの全体像

以下は、APIサーバーを実装するコード全体です。詳細な説明は後述します。

package main

import (
	"context"       // コンテキスト処理を提供する標準パッケージ
	"encoding/json" // JSON処理をサポート
	"errors"        // エラー処理の標準パッケージ
	"fmt"           // フォーマットされたI/Oを提供
	"go-api-newspaper/pkg/logger" // ロガー(ログ出力用のパッケージ)
	"log"           // ログ出力用の標準パッケージ
	"net/http"      // HTTPサーバーの実装に必要な標準パッケージ
	"os"            // OS操作用(環境変数やシグナル処理)
	"os/signal"     // OSシグナル処理を提供
	"syscall"       // システムコールシグナルを提供
	"time"          // 時間操作をサポートする標準パッケージ

	"github.com/gin-gonic/gin" // Gin Webフレームワーク
	middleware "github.com/oapi-codegen/gin-middleware" // OpenAPIリクエストバリデータ
	swaggerfiles "github.com/swaggo/files" // Swagger UIファイル
	ginSwagger "github.com/swaggo/gin-swagger" // Swagger UIをGinでラップ
	"github.com/swaggo/swag" // Swaggerの機能を提供

	"go-api-newspaper/api"             // 自作APIパッケージ
	"go-api-newspaper/app/controllers" // 自作コントローラパッケージ
	"go-api-newspaper/app/models"      // 自作モデルパッケージ
	"go-api-newspaper/configs"         // 自作設定パッケージ
)

func main() {
	// データベースの初期化
	if err := models.SetDatabase(models.InstanceMySQL); err != nil {
		// 初期化に失敗した場合、ログにエラーを記録してプログラムを終了
		logger.Fatal(err.Error())
	}

	// Ginのデフォルト設定を使用してルーターを作成
	router := gin.Default()

	// OpenAPI仕様を取得(API仕様のバリデーション用)
	swagger, err := api.GetSwagger()
	if err != nil {
		// 取得に失敗した場合、プログラムを終了
		panic(err)
	}

	// 開発環境の場合、Swagger UIを有効化
	if configs.Config.IsDevelopment() {
		swaggerJson, _ := json.Marshal(swagger) // OpenAPI仕様をJSON形式に変換
		var SwaggerInfo = &swag.Spec{
			InfoInstanceName: "swagger",
			SwaggerTemplate:  string(swaggerJson),
		}
		swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) // Swagger情報を登録
		router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) // Swagger UIエンドポイントを追加
	}

	// APIのエンドポイントを設定
	apiGroup := router.Group("/api") // ベースURLを`/api`に設定
	{
		v1 := apiGroup.Group("/v1") // バージョン1のAPIをグループ化
		{
			// OpenAPI仕様に基づくリクエストバリデーションをミドルウェアとして追加
			v1.Use(middleware.OapiRequestValidator(swagger))
			newspaperHandler := &controllers.NewspaperHandler{} // コントローラをインスタンス化
			api.RegisterHandlers(v1, newspaperHandler)          // ハンドラをエンドポイントに登録
		}
	}

	// HTTPサーバーの設定
	srv := &http.Server{
		Addr:    "0.0.0.0:8080", // サーバーをポート8080で待機
		Handler: router,         // Ginルーターをハンドラとして設定
	}

	// サーバーをバックグラウンドで起動
	go func() {
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			// サーバーが予期せず停止した場合、エラーログを出力
			logger.Fatal(err.Error())
		}
	}()

	// OSシグナルを受け取るためのチャネルを作成
	quit := make(chan os.Signal, 1)
	// シグナルをキャッチ(SIGINT: Ctrl+C, SIGTERM: 終了要求)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit // シグナルを受け取るまでブロック

	// サーバーのシャットダウン処理
	log.Println("Shutdown Server ...")
	defer logger.Sync() // ログのバッファをフラッシュ

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 2秒のタイムアウト付きコンテキスト
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		// シャットダウンに失敗した場合、エラーログを出力
		logger.Error(fmt.Sprintf("Server Shutdown: %s", err.Error()))
	}
	<-ctx.Done() // コンテキストの完了を待つ
	logger.Info("Shutdown") // シャットダウン完了をログ出力
}

詳細解説

データベースの初期化

if err := models.SetDatabase(models.InstanceMySQL); err != nil {
	// 初期化に失敗した場合、ログにエラーを記録してプログラムを終了
	logger.Fatal(err.Error())
}

models.SetDatabase(models.InstanceMySQL)を呼び出してデータベース接続を設定します。 Dockerで立ち上げたMySQLコンテナと接続するための設定が行われます。

Ginのデフォルト設定を使用してルーターを作成

router := gin.Default()
  • Ginとは?
    Golangで人気のあるWebフレームワークで、高速なAPIの構築に利用されます。

  • gin.Default()とは?
    Ginのデフォルト設定を使ってルーターを作成します。この設定には以下が含まれます:
     ・Loggerミドルウェア: リクエストをログに記録する。
     ・Recoveryミドルウェア: パニックをキャッチしてサーバーを落とさずにエラーレスポンスを返す。

  • カスタム設定にしたい場合はどうする?
    gin.New()を使用して、必要なミドルウェアを自分で追加することができます。

Swaggerのセットアップ

// OpenAPI仕様を取得(API仕様のバリデーション用)
	swagger, err := api.GetSwagger()
	if err != nil {
		panic(err)
	}

	// 開発環境の場合、Swagger UIを有効化
	if configs.Config.IsDevelopment() {
		swaggerJson, _ := json.Marshal(swagger) // OpenAPI仕様をJSON形式に変換
		var SwaggerInfo = &swag.Spec{
			InfoInstanceName: "swagger",
			SwaggerTemplate:  string(swaggerJson),
		}
		swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) // Swagger情報を登録
		router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) // Swagger UIエンドポイントを追加
	}
  • Swaggerとは?
    API仕様を記述するためのフォーマット(OpenAPI Specification)です。Swaggerを使うと、以下が可能になります:
    ・APIドキュメントの自動生成。
    ・Swagger UIを使ったAPIのインタラクティブなテスト。

  • swag.Spec とは?
    swag.SpecはSwagger仕様を表現する構造体です。
    InfoInstanceNameは登録するSwaggerの名前を指定し、SwaggerTemplateはAPI仕様をJSON形式で指定します。

  • swag.Registerとは?
    Swagger仕様を内部に登録します。この後、Ginのルート設定などでSwagger UIやAPI仕様を利用可能にします。

  • ginSwagger.WrapHandlerとは?
    Swagger UIを提供するためのハンドラを作成します。これにより、/swagger/*anyにアクセスするとSwagger UIが表示されます。

エンドポイントの登録

apiGroup := router.Group("/api")
{
	v1 := apiGroup.Group("/v1")
	{
		// OpenAPI仕様に基づくリクエストバリデーションをミドルウェアとして追加
		v1.Use(middleware.OapiRequestValidator(swagger)) // 変数swaggerのAPI仕様に基づくバリデーション
		newspaperHandler := &controllers.NewspaperHandler{}
		api.RegisterHandlers(v1, newspaperHandler) // ルーターに登録
	}
}

Ginフレームワークを使い、/api/v1にエンドポイントを設定。
OpenAPI仕様に基づきリクエストをバリデーションします。

v1.Use(middleware.OapiRequestValidator(swagger))

  • OapiRequestValidatorとは?
    OpenAPI仕様に基づいてリクエストをバリデーションするミドルウェアです。

  • 何をバリデーションするのか?
    ・リクエストのパスやメソッドが仕様と一致しているか。
    ・リクエストのパラメータやボディが仕様に準拠しているか。

これにより、不正なリクエストがAPIサーバーに到達するのを防ぎます。

api.RegisterHandlers(v1, newspaperHandler)とは?

OpenAPIで定義したエンドポイントとハンドラをルーターに登録します。これにより、指定されたエンドポイントにリクエストが来たときに、対応するハンドラが実行されます。

サーバーの起動

srv := &http.Server{
	Addr:    "0.0.0.0:8080",
	Handler: router,
}
  • http.Serverとは?
    Goの標準ライブラリで提供されるHTTPサーバーの設定構造体です。
go func() { // ListenAndServe サーバーを起動しリクエストをまつ
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		logger.Fatal(err.Error())
	}
}()
  • srv.ListenAndServe()とは?
    サーバーを指定したアドレスとポートで起動します。

  • errors.Isとは?
    エラーが特定の型や内容に一致しているかを判定します。

  • http.ErrServerClosedとは?
    サーバーが正常にシャットダウンされたことを示すエラーです。このエラー以外の場合に致命的なログを出力します。

別ゴルーチンで実行され、メインの実行フローに影響を与えません。

チャンネルでシグナルをキャッチ

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
  • チャンネルとは?
    Goの並行処理で、ゴルーチン間の通信を行うためのデータ構造です。

  • なぜこれをするのか?
    システムのシグナル(例:Ctrl+C)をキャッチしてサーバーを優雅にシャットダウンします。

サーバーのシャットダウン開始

log.Println("Shutdown Server ...")
defer logger.Sync() // ログのバッファをフラッシュする
  • シグナルを受信してサーバーのシャットダウン処理を開始したことをログに出力します。これはすぐに実行されます。
  • logger.Sync()は、バッファ内に溜まったログを出力するための処理です。

タイムアウト付きのコンテキストを作成

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 2秒のタイムアウトを持つコンテキスト
defer cancel()
  • context.WithTimeout(context.Background(), 2*time.Second)とは?
    2秒のタイムアウトを持つコンテキストを作成します。
    サーバーのシャットダウン処理が完了しない場合でも、2秒後には強制的に処理を終了させる仕組みを提供します。

  • defer cancel()とは?
    コンテキストのキャンセル関数を登録し、プログラム終了時にリソースを解放します。この時点では実行されません。

サーバーのシャットダウン

if err := srv.Shutdown(ctx); err != nil {
    logger.Error(fmt.Sprintf("Server Shutdown: %s", err.Error()))
}
  • srv.Shutdown(ctx)とは?
    サーバーを優雅にシャットダウンします。
    未処理のリクエストを完了させる時間を確保します。
    タイムアウト(2秒)が経過した場合、未完了のリクエストを切り捨てて終了します。

  • エラーチェック
    シャットダウン中にエラーが発生した場合、ログにエラーメッセージを出力します。

コンテキストの終了を待つ

<-ctx.Done()
logger.Info("Shutdown")
  • コンテキストが終了(タイムアウトまたは処理完了)するまで待機します。
    srv.Shutdown(ctx)が成功すると、ctx.Done()が閉じられます。
    タイムアウトが発生した場合も同様に閉じられます。

まとめ

このコードは、APIサーバー構築の基本をしっかりと押さえています。
DockerでのMySQL連携やSwaggerによるAPI仕様の可視化、シグナル対応による優雅なシャットダウンなど、実用的な要素が含まれています。

ぜひ、このコードをコピーして、実際に動かしてみてください!
APIサーバーの作成がぐっと身近になります。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?