はじめに
Go × Ginの基礎を押さえたら、いよいよ次のステップに進みます。
この応用編では、実際のAPI開発で役立つテクニックや、将来的にスケーラブルなシステムを作るための設計のヒントをまとめます。
具体的には以下のようなテーマを扱います。
- 認証やバリデーションの実装例
- エラーハンドリングのパターン
- Ginアプリケーションのテスト手法
- カスタムミドルウェアの作り方
- スケーラブルなAPI設計への展望
これらを踏まえ、実務でも迷わない基礎力をさらに強化していきます。
Ginの基礎を知りたい方は、こちらの記事をご覧ください。
Ginでよく使う便利な機能
バインディング(Binding)
Ginは、リクエストのパラメータを構造体にマッピング(バインディング)できます。
以下は、クエリパラメータを構造体にバインディングする例です。
type QueryParams struct {
Name string `form:"name"`
Age int `form:"age"`
}
r.GET("/bind", func(c *gin.Context) {
var params QueryParams
if err := c.ShouldBindQuery(¶ms); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"name": params.Name, "age": params.Age})
})
curl 'http://localhost:8080/bind?name=Gopher'
レスポンス
{"age":0,"name":"Gopher"}
バリデーション(Validation)
Ginは binding タグを使ってバリデーションも行えます。
type User struct {
Name string `json:"name" binding:"required"`
Age int `json:"age" binding:"gte=0,lte=120"`
}
r.POST("/validate", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"name": user.Name, "age": user.Age})
})
binding:"required"
→ 必須チェックbinding:"gte=0,lte=120"
→ 0以上120以下の範囲をチェック
リクエスト
curl -X POST http://localhost:8080/validate \
-H "Content-Type: application/json" \
-d '{"name": "Gopher", "age": 5}'
レスポンス
{"age":5,"name":"Gopher"}
リクエスト(ageを範囲外の数字に変更)
curl -X POST http://localhost:8080/validate \
-H "Content-Type: application/json" \
-d '{"name": "Gopher", "age": 999}'
レスポンス
{"error":"Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag"}
カスタムバリデータの作成
標準のバリデーションでは足りない場合は、独自のバリデーションロジックを登録できます。
package main
import (
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/gin-gonic/gin/binding"
)
func main() {
r := gin.Default()
// カスタムバリデーション登録
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("customTag", customValidation)
}
r.POST("/custom", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"name": user.Name})
})
r.Run()
}
type User struct {
Name string `json:"name" binding:"required,customTag"`
}
// カスタムバリデーション関数
func customValidation(fl validator.FieldLevel) bool {
value := fl.Field().String()
return len(value) >= 3 // 3文字以上で合格
}
リクエスト
curl -X POST http://localhost:8080/custom \
-H "Content-Type: application/json" \
-d '{"name":"Gopher"}'
レスポンス
{"name":"Gopher"}
リクエスト(条件に合わない「3文字未満」の例)
curl -X POST http://localhost:8080/custom \
-H "Content-Type: application/json" \
-d '{"name":"Go"}'
レスポンス
{"error":"Key: 'User.Name' Error:Field validation for 'Name' failed on the 'customTag' tag"}
構造化されたハンドラの設計
Ginを使ったAPI開発が進むにつれて、ハンドラの構造化が重要になってきます。
ここでは、ハンドラを疎結合で保守しやすくするための考え方や、依存性注入の基本例を見ていきます。
なぜ構造化が必要
ハンドラを func(c *gin.Context) のまま書いていくと、以下のような課題が出てきます。
- データベース接続や外部サービスの依存が増える
- テストしにくい
- 可読性が下がる
ハンドラを構造体としてまとめ、ビジネスロジックをサービス層に分離することで、これらの問題を防ぎます。
サービス層とは?
サービス層は、アプリケーションのビジネスロジックを担当する部分です。
具体的な例として、ユーザー情報を扱う UserService
インターフェースを用意することが多いです。
type UserService interface {
GetByID(id string) (User, error)
}
構造体ハンドラの例
ハンドラは、サービス層の依存を注入して初期化します。
type UserHandler struct {
userService UserService
}
func NewUserHandler(service UserService) *UserHandler {
return &UserHandler{userService: service}
}
func (h *UserHandler) GetUser(c *gin.Context) {
id := c.Param("id")
user, err := h.userService.GetByID(id)
if err != nil {
c.JSON(404, gin.H{"error": "User not found"})
return
}
c.JSON(200, user)
}
この例では、ハンドラは UserService
に仕事を任せており、処理の中心はサービス層に移っています。
ルーティングでの利用
実際にルーティングで利用するときは、依存関係を組み合わせて渡します。
// 例: main.go
repo := NewUserRepository()
userService := NewUserService(repo)
userHandler := NewUserHandler(userService)
r.GET("/users/:id", userHandler.GetUser)
- UserRepository: データアクセス担当
- UserService: ビジネスロジック担当
- UserHandler: HTTPのやりとりを担当
このように分離することで、役割が明確になります。
構造化のメリット
- 依存性が明確化し、コードの見通しが良くなる
- モックの注入がしやすくなり、テストが容易に
- 役割ごとに保守・拡張しやすくなる
補足:実装例
上記イメージを実際のコードに落としたものとなります。
ディレクトリ構成(例)
go-gin-test-guide/
├── main.go
├── handler/
│ └── user_handler.go
├── service/
│ └── user_service.go
├── repository/
│ └── user_repository.go
└── model/
└── user.go
package model
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
package repository
import "my-gin-app/model"
type UserRepository interface {
FindByID(id string) (*model.User, error)
}
type userRepositoryImpl struct {
// 実際にはDB接続などを持つが、今回は省略
}
func NewUserRepository() UserRepository {
return &userRepositoryImpl{}
}
func (r *userRepositoryImpl) FindByID(id string) (*model.User, error) {
// 例として、静的なユーザー情報を返す
if id == "1" {
return &model.User{ID: "1", Name: "Gopher"}, nil
}
return nil, nil // 見つからなかった場合
}
package service
import (
"errors"
"my-gin-app/model"
"my-gin-app/repository"
)
type UserService interface {
GetByID(id string) (*model.User, error)
}
type userServiceImpl struct {
repo repository.UserRepository
}
func NewUserService(repo repository.UserRepository) UserService {
return &userServiceImpl{repo: repo}
}
func (s *userServiceImpl) GetByID(id string) (*model.User, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("user not found")
}
return user, nil
}
package handler
import (
"net/http"
"my-gin-app/service"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
userService service.UserService
}
func NewUserHandler(service service.UserService) *UserHandler {
return &UserHandler{userService: service}
}
func (h *UserHandler) GetUser(c *gin.Context) {
id := c.Param("id")
user, err := h.userService.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, user)
}
package main
import (
"my-gin-app/handler"
"my-gin-app/repository"
"my-gin-app/service"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
userRepo := repository.NewUserRepository()
userService := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userService)
// ルーティング
r.GET("/users/:id", userHandler.GetUser)
r.Run(":8080")
}
リクエスト(ユーザーが見つかる場合)
curl http://localhost:8080/users/1
レスポンス
{"id":"1","name":"Gopher"}
リクエスト
curl http://localhost:8080/users/999
レスポンス
{"error":"User not found"}
テスト手法
Ginを使ったAPI開発では、以下の3つのレイヤーに分けてテストを設計すると堅牢です。
- ハンドラのテスト
- サービス層・リポジトリ層のテスト
- インテグレーションテスト(統合テスト)
それぞれのテスト手法とサンプルコード例を以下にまとめます。
ハンドラのテスト
Ginのハンドラをテストする際は、以下を意識します。
- 実際のHTTPリクエストを模倣できる
httptest.NewRecorder
- 必要に応じて依存するサービスやリポジトリをモック化する(今回の例ではサービスをモックにする)
モックの定義
まず、service.UserService
をモック化します。
これにより、ハンドラの振る舞いだけをテストできます。
package handler_test
import (
"errors"
"go-gin-basic-guide/handler"
"go-gin-basic-guide/model"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
// モックサービス
type mockUserService struct {
getByIDFunc func(id string) (*model.User, error)
}
func (m *mockUserService) GetByID(id string) (*model.User, error) {
return m.getByIDFunc(id)
}
ハンドラのテストコード
実際に /users/:id
エンドポイントをテストします。
func TestGetUser_Success(t *testing.T) {
// Ginをテストモードにする
gin.SetMode(gin.TestMode)
// モックサービスを準備
mockService := &mockUserService{
getByIDFunc: func(id string) (*model.User, error) {
return &model.User{ID: id, Name: "Test User"}, nil
},
}
// ハンドラを生成
userHandler := handler.NewUserHandler(mockService)
// Ginのルーターにハンドラを設定
r := gin.Default()
r.GET("/users/:id", userHandler.GetUser)
// リクエストを作成
req, _ := http.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
// リクエストを実行
r.ServeHTTP(w, req)
// 結果を検証
assert.Equal(t, http.StatusOK, w.Code)
assert.JSONEq(t, `{"id":"1","name":"Test User"}`, w.Body.String())
}
func TestGetUser_NotFound(t *testing.T) {
gin.SetMode(gin.TestMode)
mockService := &mockUserService{
getByIDFunc: func(id string) (*model.User, error) {
return nil, errors.New("user not found")
},
}
userHandler := handler.NewUserHandler(mockService)
r := gin.Default()
r.GET("/users/:id", userHandler.GetUser)
req, _ := http.NewRequest("GET", "/users/999", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.JSONEq(t, `{"error":"User not found"}`, w.Body.String())
}
テスト実行
go test -v ./handler
テストが正常に終了しました。
最後にリファクタリングとして共通のテスト用ルーター初期化関数を作っておきます。
func setupRouter(handlerFunc gin.HandlerFunc) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.Default()
r.GET("/users/:id", handlerFunc)
return r
}
func TestGetUser_Success(t *testing.T) {
mockService := &mockUserService{
getByIDFunc: func(id string) (*model.User, error) {
return &model.User{ID: id, Name: "Test User"}, nil
},
}
userHandler := handler.NewUserHandler(mockService)
r := setupRouter(userHandler.GetUser)
req, _ := http.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.JSONEq(t, `{"id":"1","name":"Test User"}`, w.Body.String())
}
func TestGetUser_NotFound(t *testing.T) {
mockService := &mockUserService{
getByIDFunc: func(id string) (*model.User, error) {
return nil, errors.New("user not found")
},
}
userHandler := handler.NewUserHandler(mockService)
r := setupRouter(userHandler.GetUser)
req, _ := http.NewRequest("GET", "/users/999", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.JSONEq(t, `{"error":"User not found"}`, w.Body.String())
}
テストパターンが増えた際にはこのような共通関数が必要となってきます。
サービス層のテスト
サービス層のテストでは、リポジトリをモック化してビジネスロジックのみを検証します。
例として service/user_service.go
のテストコードを紹介します。
package service_test
import (
"go-gin-basic-guide/model"
"go-gin-basic-guide/service"
"testing"
"github.com/stretchr/testify/assert"
)
// リポジトリのモック
type mockUserRepo struct {
findByIDFunc func(id string) (*model.User, error)
}
func (m *mockUserRepo) FindByID(id string) (*model.User, error) {
return m.findByIDFunc(id)
}
func TestGetByID_Success(t *testing.T) {
repo := &mockUserRepo{
findByIDFunc: func(id string) (*model.User, error) {
return &model.User{ID: id, Name: "Mock User"}, nil
},
}
userService := service.NewUserService(repo)
user, err := userService.GetByID("1")
assert.NoError(t, err)
assert.Equal(t, "Mock User", user.Name)
}
func TestGetByID_NotFound(t *testing.T) {
repo := &mockUserRepo{
findByIDFunc: func(id string) (*model.User, error) {
return nil, nil
},
}
userService := service.NewUserService(repo)
user, err := userService.GetByID("999")
assert.Nil(t, user)
assert.EqualError(t, err, "user not found")
}
✅ ポイント
- サービス層では、ビジネスロジックに関わる条件分岐を中心にテスト
- データアクセスはモック化(リポジトリ層に依存しない)
テスト実行
go test -v ./service
リポジトリ層のテスト
リポジトリ層では、実際のDB(例: SQLiteやテスト用MySQL)を使うか、インメモリDBを使うことが多いです。
小さい場合はテーブルモックなどを使う例もあります。
例(簡略化した考え方):
func TestFindByID_RealDB(t *testing.T) {
db := setupTestDB() // 例: SQLiteのメモリDBを初期化
repo := repository.NewUserRepositoryWithDB(db)
// 事前にテストデータを作成
db.Exec("INSERT INTO users (id, name) VALUES (?, ?)", "1", "Test User")
user, err := repo.FindByID("1")
assert.NoError(t, err)
assert.Equal(t, "Test User", user.Name)
}
統合テスト(インテグレーションテスト)
「実際にサーバーを立ち上げて、APIのリクエスト&レスポンスを検証するテスト」です。
httptest.NewServer
を使うことで、サーバーを本番に近い形で起動してテストできます。
package integration_test
import (
"go-gin-basic-guide/handler"
"go-gin-basic-guide/repository"
"go-gin-basic-guide/service"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
// テストサーバーをセットアップ
func setupIntegrationRouter() *gin.Engine {
repo := repository.NewUserRepository()
userService := service.NewUserService(repo)
userHandler := handler.NewUserHandler(userService)
r := gin.Default()
r.GET("/users/:id", userHandler.GetUser)
return r
}
func TestGetUserIntegration_Success(t *testing.T) {
gin.SetMode(gin.TestMode)
r := setupIntegrationRouter()
ts := httptest.NewServer(r)
defer ts.Close()
resp, err := http.Get(ts.URL + "/users/1")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
// レスポンスボディも検証
defer resp.Body.Close()
body := make([]byte, resp.ContentLength)
resp.Body.Read(body)
assert.JSONEq(t, `{"id":"1","name":"Gopher"}`, string(body))
}
func TestGetUserIntegration_NotFound(t *testing.T) {
gin.SetMode(gin.TestMode)
r := setupIntegrationRouter()
ts := httptest.NewServer(r)
defer ts.Close()
resp, err := http.Get(ts.URL + "/users/999")
assert.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
defer resp.Body.Close()
body := make([]byte, resp.ContentLength)
resp.Body.Read(body)
assert.JSONEq(t, `{"error":"User not found"}`, string(body))
}
✅ ポイント
- Ginサーバーを立ち上げて、実際にHTTPリクエストを送る
- 依存も「実装」なので、サービス層〜リポジトリ層まで一気通貫でテスト
- E2E(外部連携含むテスト)よりは範囲が狭いが、「アプリ内部の統合確認」に最適
テスト実行
go test -v ./integration
Ginのグルーピング・ルーティング活用術
バージョニング
共通パス/認証ルールの整理
エラーハンドリングのパターン
エラーレスポンスの統一化
GinのError型の活用
認証・認可の実装例
JWT認証
ミドルウェアとしての実装
よりスケーラブルな設計へ
サービス分割、マイクロサービス化へのステップ
将来的なアーキテクチャの展望
まとめと振り返り
関連する技術ブログ
Go × Echoで始めるWebサーバ構築入門:シンプル・高速なAPI開発を最短で学ぶ
Go言語で軽量・高速なWebアプリケーションを構築したい方へ。人気フレームワーク「Echo」の導入から基本構造、ルーティング、レスポンス、ミドルウェアの使い方までを、実践的なサンプルコードとともにわかりやすく解説します。Ginとの違いにも触れながら、最小構成でWebサーバを立ち上げるまでを丁寧にガイドします。
shinagawa-web.com
Go × Gin 基礎編:高速APIサーバーの作り方を徹底解説
Go言語の特性を活かし、Ginフレームワークでシンプルかつ強力なAPIサーバーを構築する方法をステップバイステップで解説します。インストールからミドルウェア、サンプルAPI作成まで、実践的な内容を盛り込んだ入門記事です。
shinagawa-web.com
Go × Gin でMVC構成のブログ記事投稿用Web APIを構築する:基礎からスケーラブル設計まで
Go言語とGinフレームワークを使って、MVC構成のWeb APIを一から構築していきます。ディレクトリ設計からルーティング、ミドルウェアの活用まで、実践的なコードで丁寧に解説。スケーラブルな設計の基本を学びたい方におすすめの入門編です。
shinagawa-web.com
Go × Gin × MVC構成で実践する堅牢なテスト設計と実装ガイド
単体テスト、統合テスト、E2Eテストを通して、GinベースのMVC構成アプリを堅牢に育てる方法を解説します。httptestの使い方やモックの作り方、共通処理の抽出など、実務に活かせるテスト設計のエッセンスを凝縮しました。
shinagawa-web.com
Go + Ginアプリを本番品質に仕上げる:設定・構成・CI導入まで
アプリを「動く」から「本番で保てる」品質へ引き上げるために、設定ファイルの整理、依存の分離、GitHub ActionsによるCI導入までを解説します。小規模開発からチーム開発へスムーズに移行したい方に向けた応用編です。
shinagawa-web.com
Goで文字列スライス処理を快適に:strlistutils v1.0.0 をリリースしました
文字列スライス処理をシンプルかつ強力にするGoパッケージ「strlistutils」の v1.0.0 を公開しました。重複削除・トリミング・フィルタ・マッピングなど、よくある操作を標準的な方法で安全・高速に扱えます。CI/Fuzz/Benchmarkも対応済み。
shinagawa-web.com
Go言語でWeb APIサーバーを作る完全ガイド|設計・開発フロー・テスト・CI/CDまで徹底解説
Go言語でWeb APIサーバーを作りたい方向けに、設計方針、APIスキーマ(OpenAPI/Swagger)、エンドポイント実装、テスト(単体・統合・E2E)、ミドルウェアの設定、CI/CD自動化までを詳しく解説します。初心者でもわかりやすく、開発フローを段階的にまとめた完全ガイドです。
shinagawa-web.com
弊社の技術支援サービス
無駄なコストを削減し、投資対効果を最大化する
クラウド費用の高騰、不要なSaaSの乱立、開発工数の増加――これらの課題に悩んでいませんか?本サービスでは、クラウドコストの最適化、開発効率向上、技術選定の最適化 を通じて、単なるコスト削減ではなく、ROIを最大化する最適解 をご提案します。
shinagawa-web.com
最新技術の導入・検証を支援するPoCサービス
Remix、React Server Components、TypeScript移行、クラウドサービス比較、マイクロサービス、サーバーレス、デザインシステムなど、最新技術のPoC(概念実証)を通じて、最適な技術選定と導入を支援します。貴社の開発課題に合わせた検証・実装で、ビジネスの成長を加速させます。
shinagawa-web.com
開発生産性を最大化するための技術支援
開発チームの生産性向上、コードの品質管理、インフラの最適化まで、様々な側面からサポートします。コードベースのリファクタリングから、テスト自動化、オンボーディング強化まで、プロジェクトの成功に必要なすべての支援を提供。御社の開発現場が効率的に機能するように、技術的な障害を取り除き、スムーズな開発を実現します。
shinagawa-web.com
開発品質向上支援 – 効率的で安定したプロダクトを実現
フロントエンドからバックエンド、データベースまで、開発プロセス全体を最適化し、安定したプロダクト作りをサポートします。コードレビューの仕組み、型定義の強化、E2Eテスト環境の構築など、開発の各ステップにおけるベストプラクティスを導入することで、より効率的でバグの少ない、そしてユーザー満足度の高いサービス提供を支援します。
shinagawa-web.com
Webアプリのセキュリティ強化支援
Webアプリの脆弱性対策からインフラのセキュリティ強化まで、包括的なセキュリティ支援を提供。OWASP Top 10対策、JWT認証の最適化、APIのアクセス制御、依存パッケージの監査、セキュアコーディングの標準化など、実践的なアプローチで開発現場の安全性を向上させます。
shinagawa-web.com
目次
お問い合わせ