はじめに
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サーバーの作成がぐっと身近になります。