みなさんAPIを開発したことはありますか?何かしらのフレームワークやライブラリを用いてバックエンドを開発することが多くあるのではないでしょうか?近年,Webサービスやスタートアップの初期開発から大規模なシステムへの拡張において,Ruby on RailsとGoはしばしば比較の対象となる.Railsは2000年代中盤から一世を風靡し,Webフレームワークとして確固たる地位を築いた.一方で,Go言語は2010年代半ば以降にマイクロサービスやクラウドネイティブな環境で徐々に存在感を増し,軽量で高速なWebアプリケーションサーバを求める声に応える形で普及してきた.この2つのスタックを選択肢に入れる企業や開発者は多く,最近ではRailsで迅速なプロトタイピング・開発を行い,スケールが求められる箇所をGoでマイクロサービス化する,といった使い分け事例も見られる.
RailsとGoのAPI構築の概念
RailsとGoを用いる際,Railsは標準的なMVCアーキテクチャが確立されており,Active RecordやAction Pack,Action Mailerなど,Webアプリケーションに必要な一通りの機能が揃っている.また,Railsの哲学である「Convention over Configuration(規約による設定軽減)」は,開発者に学習コストはあるものの,一度流儀を掴めば爆発的な生産性を発揮できる.それに対して,Goは言語自体が型安全でシンプルな構文,並行処理を容易に行うgoroutineやチャネルを備えており,高いパフォーマンスと軽量なデプロイが可能である.Goにおける有名なWebフレームワークの一つがGinである.Ginは軽量なHTTPルータおよびミドルウェア機能を提供し,必要最低限のセットアップで高速なAPIサーバを立ち上げられるのが特徴だ.
Railsはフルスタックフレームワークであり,デフォルトで多くの機能が搭載されている.スキャフォールディングで基本的なCRUDのコード生成が可能で,Active Recordでデータベース操作を抽象化できる.テストや環境設定,i18n,多言語対応,アセットパイプラインなど,開発・運用に必要なパーツを網羅している.一方,Ginは「バッテリー同梱」的なRailsとは異なり,ルーティングやミドルウェア以外は開発者が選定して組み合わせる.ORMはGORMを始めとする外部ライブラリを利用し,認証は別のパッケージ,バリデーションはさらに別のパッケージ,といったように,必要なコンポーネントを用途に合わせて選択できる.これにより,Goのコードは軽量な単一バイナリとしてビルドされ,Dockerイメージも最小限の大きさで運用できるという利点がある.
Ruby(Rails)とGo(Gin)の一般的なアーキテクチャ
RailsはMVC(Model, View, Controller)が標準で,app/models, app/controllers, app/viewsといった明確なディレクトリ構成に従う.コントローラではルーティングされたリクエストを受け取り,モデルを介してデータを取得し,ViewあるいはJSONを返すというフローが一般的だ.特にAPIモードのRailsでは,ViewレイヤーをシンプルなJSONテンプレート処理に限定できるため,APIサーバとしても有用である.また,Active Recordでモデル層とDB操作が紐づいており,慣れれば非常に直感的なコードでデータ操作が可能となる.
Ginはフォルダ構成に決まったルールは無く,自由度が高い.そのため,Clean ArchitectureやHexagonal Architectureといったモダンなアーキテクチャパターンを採用しやすい特性がある.たとえば,domain,usecase,infrastructure,interfacesなどの層に分けて整理し,domainパッケージでビジネスロジックやエンティティ定義を行い,usecaseパッケージでユースケース層のロジックをまとめ,infrastructureパッケージでDB接続や外部APIとの通信を実装する.そしてinterfacesパッケージにGinのハンドラを置いて,ルーティング,入力パラメータ処理,レスポンス形成といったUI層を構築する.こうした分離により,テスト容易性や変更への柔軟性が大幅に向上する.
ドメイン駆動設計(DDD)とGin
Ginにおいて,DDDパターンを取り入れることで,大規模プロジェクトや長期保守が求められるシステムでも見通しの良いアーキテクチャが実現できる.RailsもDDDを適用できなくはないが,RailsはMVCおよびActive Recordによるアプリケーション中心の構造が強く,モデルとドメインロジックが密結合になりがちである.その点,Go+Ginの組み合わせは,初期状態では構造に何も制約がないため,以下のようなレイヤ構成が可能だ.
例として,ユーザ情報を扱うドメインを考える.
-
domainパッケージ
Userエンティティ(User構造体)やドメインサービス(ビジネスロジック)を定義する.UserはユーザID,名前,メールアドレスなどのフィールドと,それを操作するためのメソッドを持つ.ここではデータベースの存在を意識しない. -
repositoryパッケージ
Userリポジトリインタフェースを定義し,それを満たす具体的な実装(DBリポジトリ)をinfrastructure層などで提供する.こうすることでdomain層はDB実装から独立する. -
usecaseパッケージ
アプリケーションが提供するユースケース(例えば「ユーザ情報を取得して返す」)を定義する.usecaseはdomainインタフェースを呼び出して必要なドメインロジックを実行し,結果を返す.ここでも特定のフレームワークやライブラリには依存せず,単なるGoのコードとして記述できる. -
interfacesパッケージ
Ginのハンドラを配置する層であり,HTTPのリクエスト・レスポンスを扱う.ハンドラはusecaseを呼び出し,戻ってきた結果をJSONレスポンスとして整形し,HTTPステータスコードを設定して返す.また,URLパラメータやクエリパラメータのバリデーションを行い,エラー時はHTTP 400や404など適切なステータスを返す.こうしたUI層から下位層に依存関係を一方向に保つことで,テストやリファクタリングが容易になる.
Railsでの実装例とGinでの実装例
Railsで単純なユーザ取得API:
get "users/:id", to: "users#show"
class UsersController < ApplicationController
def show
user = User.find(params[:id])
render json: { id: user.id, name: user.name }
end
end
class User < ApplicationRecord
end
RailsはこれだけでGET /users/:idでユーザ情報をJSON出力可能だ.Active Recordを用いてDB操作は非常にシンプルになる.
GinでDDDを取り入れた例(概念的なフォルダ構成と処理フロー):
project/
domain/
model/
user_model.go // Userエンティティやドメインロジックを定義
repository/
user_repository.go // User関連ドメインサービスがあれば定義
mock/
user_mock.go // userのmockデータを格納
repository/
user_repository.go // Userリポジトリインタフェース定義
infrastructure/
dao/
user_dao.go // GORMを用いたUserリポジトリのDataAccessObjectの実装
db.go // DB接続関連処理
usecase/
user_usecase.go // ユースケース(例えばGetUserByID)定義
interface/
handler/
user_handler.go // Ginハンドラを定義
router.go // ルーティング定義
コード例は以下の通りである.
returnの際の構造体はわかりやすくするため別で定義していません.直接returnに構造体の定義をしています
package model
type User struct {
ID int
Name string
}
package repository
type UserRepository interface {
FindByID(id int) (*User, error)
}
package mock
import (
"project/domain"
)
type MockUserRepository struct {
users map[int]*domain.User
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: map[int]*domain.User{
1: {ID: 1, Name: "Alice"},
2: {ID: 2, Name: "Bob"},
},
}
}
func (m *MockUserRepository) FindByID(id int) (*model.User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, nil
}
package dao
import (
"errors"
"project/domain/model"
"gorm.io/gorm"
)
type UserDAO struct {
db *gorm.DB
}
func NewUserDAO(db *gorm.DB) *UserDAO {
return &UserDAO{db: db}
}
func (dao *UserDAO) FindByID(id int) (*model.User, error) {
var u struct {
ID int
Name string
}
if err := dao.db.Table("users").Where("id = ?", id).First(&u).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &model.User{ID: u.ID, Name: u.Name}, nil
}
package usecase
import (
"project/domain/model"
"project/domain/repository"
)
type UserUsecase struct {
repo repository.UserRepository
}
func NewUserUsecase(repo reposiory.UserRepository) *UserUsecase {
return &UserUsecase{repo: repo}
}
func (uc *UserUsecase) GetUserByID(id int) (*model.User, error) {
return uc.repo.FindByID(id)
}
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"project/usecase"
)
type UserHandler struct {
uc *usecase.UserUsecase
}
func NewUserHandler(uc *usecase.UserUsecase) *UserHandler {
return &UserHandler{uc: uc}
}
func (h *UserHandler) GetUserByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
user, err := h.uc.GetUserByID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Server error"})
return
}
if user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, gin.H{"id": user.ID, "name": user.Name})
}
package interface
import (
"github.com/gin-gonic/gin"
"project/interfaces/handler"
"project/usecase"
)
func SetupRouter(u *usecase.UserUsecase) *gin.Engine {
r := gin.Default()
userHandler := handler.NewUserHandler(u)
r.GET("/users/:id", userHandler.GetUserByID)
return r
}
return r
}
package main
import (
"log"
"project/infrastructure"
"project/interfaces"
"project/usecase"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func main() {
// DB初期化
db, err := gorm.Open(sqlite.Open("example.db"), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect database: %v", err)
}
// DBにユーザーテーブルがあるものと仮定(無ければマイグレーションや初期データ投入が必要)
userDAO := infrastructure.NewUserDAO(db)
// Usecase作成
userUC := usecase.NewUserUsecase(userDAO)
// ルーター設定
r := interfaces.SetupRouter(userUC)
// サーバー起動
if err := r.Run(":8080"); err != nil {
log.Fatalf("failed to run server: %v", err)
}
}
例えばinterfaces/handlers/user_handler.go
では,GinのハンドラでIDパラメータを受け取り,usecase層のGetUserByIDを呼び出して結果を返す.usecase層はdomain層とrepositoryインタフェースを通じてデータ取得を行うため,domain→repository→usecase→infrastructureの流れでDB操作が行われる.
このアーキテクチャでは,たとえDBアクセスのロジックを変えようが,Gin以外のフレームワーク(FiberやChi)に差し替えようが,domainとusecase層には影響が少ない.テスト時はMockリポジトリを用意してdomainやusecaseをハンドラやDB実装から分離したテストが容易に実施できる.
パフォーマンスとスケーラビリティ
Railsは包括的な機能を持つ反面,アプリケーションの規模が大きくなると若干重たくなりがちだ.しかし,キャッシング,ロードバランシング,CDN,RedisやMemcachedなどのミドルウェア活用,DockerやKubernetesによるスケールアウトなどで大規模トラフィックにも対応可能である.
GinはGo言語の特性を活かし,高速なHTTPサーバとして機能する.Goはコンパイル言語であり,ガベージコレクションも洗練されており,軽量な並行処理を実現できる.このため,短時間で大量のリクエストを捌きたいケースや,マイクロサービスとして小さなコンテナイメージで迅速にスケールするケースなどで顕著な強みを示す.DDDパターンを組み込むことで,スケール要求に応じてドメインやユースケース層を拡張しても,インフラ層やハンドラ層との分離が保たれ,保守性や性能調整のしやすさが増す.
学習コスト,エコシステム
Railsは豊富なドキュメント,コミュニティ,Gemが存在し,歴史も長く,やや独特なRails流儀を身につければ比較的楽にアプリケーションを構築できる.一方,Go+Ginは言語自体がシンプルなため習得も容易で,Ginは軽量で理解しやすい.DDDやClean Architectureを取り入れたい場合,Railsはある程度Rails流に逆らう実装が必要となるが,Ginでは最初からアーキテクチャを自由に組み立てられるため,DDD実践は比較的行いやすい.ただし,GoエコシステムはRailsほど「これ一本で全て解決」的なパッケージは少ないため,ORMや認証,バリデーション等を自分で選定して組み合わせる手間はある.しかし,その分組織やプロジェクト特性に合わせて最適化できるのが利点である.
デプロイと運用
RailsアプリはHerokuやPassenger,Capistrano,Dockerなどデプロイ手段が豊富だが,どうしてもRubyランタイムやGemインストールが必要で,コンテナイメージはやや肥大化しがちである.一方,Go+Ginでは単一のバイナリをビルドし,alpineベースのDockerイメージに格納するだけで軽快なデプロイが可能だ.Kubernetes環境でのPod起動やスケールも高速なため,マイクロサービスの一部として多数のインスタンスを立ち上げ,負荷状況に応じて柔軟にスケールアウトすることが容易である.
結局...
RailsとGinは共に優れたWeb/APIフレームワークであり,選択はプロジェクト要件やチーム構成,スケール戦略によって変わる.Railsは包括的な機能と豊富なエコシステムによって開発生産性を最大化し,比較的スムーズに中規模までのアプリケーション構築を行える.GinはGo言語特有のパフォーマンスや軽量なデプロイ性と,自由にアーキテクチャを組み立てられる特性を活かし,マイクロサービスやDDDパターンを取り入れた堅牢で拡張性のあるAPIサーバ構築に向いている.
もし,Railsでモノリシックに開発を始め,ある時点で高トラフィックや大規模分散が要求される状況に直面した場合,DDD的なアーキテクチャを採用したGo+Ginへの移行は有力な手段となるだろう.逆に,素早いプロトタイピングや小・中規模なサービスではRailsが大いに開発スピードを後押しする.また,アーキテクチャの分離やドメイン駆動設計に興味があるなら,初めからGo+Ginで清潔な分離を意識した設計を行うことで,長期的な保守性と拡張性を確保できる.
要するに,RailsとGinは目的や背景,要求事項に応じて使い分けるべきであり,フレームワークが持つ思想や特性,そしてアーキテクチャスタイル(DDDやClean Architectureなど)を踏まえて最適な判断を行うことが重要である.