個人開発のWebサービスをRuby on RailsからGo言語へ移行している


  • ページ公開日: 2025年11月29日 (土)
  • 書いた人: shimbaco

Annict (アニクト) という見たアニメが記録できるWebサービスを個人開発しています。
このサービスはRailsで開発されており、ソースコードを公開しています。

https://github.com/annict/annict

最近このRailsアプリをGo言語で書き直し始めています。
このページではその背景やどのようにやっているかなどについてご紹介します。

  • なぜRailsからGoに移行することにしたのか
  • どのように移行を進めているか
  • インフラ構成とプロジェクト構成
  • 使用しているライブラリ
  • 移行して良かったところ・不安なところ

なぜGoに移行することにしたのか?

Rubyはとても好きですし、Railsの生産性の高さは今でも最高だと思っています。
ただ、理想は以下のような言語が良いなと思っていました。

  • 最初から静的型付け
  • メモリ使用量が少ない
  • 読みやすく書きやすい
  • 実行速度が速い
  • ビルドが速い
  • Webアプリを作る土壌が整っている

Rubyでも型を使いたかったのでSorbetを導入していました。
Sorbetもとても良いですが、言語レベルで型が組み込まれているほうがすっきり書けますし、外部ライブラリも含めて型が保証されている安心感があります。

リソース効率についても重視しています。
Annictなどの僕が運営しているサービスは、コスパを重視してVPS1台で運用しています。
スケールしたくなったときは強いサーバーにスケールアップするというVPS1台でいけるところまでいく作戦です。
長く運営するためにサーバー費用はなるべく安くしたいので、処理に求められるマシンリソースは少なければ少ないほど嬉しいです。

また、最近はAIにコードを書いてもらうことがほとんどになりました。
自分で書いていたころはコードの記述量は少ないほうがタイプ数も少なく嬉しかったのですが、最近ではそのあたりのコストはほとんど気にならなくなってきました。
コードの記述量を重視しなくなった代わりに、現在はAI (や他人) が書いたコードがどれだけ理解しやすいか?のほうを重要視し始めています。

上記のように考えた結果、現状Goが一番バランスが良いかなと思い、Goを選びました。

どうやってGoに移行しているか?

Claude Codeを使う

コードの記述にはClaude Codeを使っています。
他のツールはあまり触ったことがなく、UIが柔和で優しい感じが好みで使っているくらいの感じです。

Goはほとんど触れたことがなかったので、他のGoプロジェクトを見たり、Claude Codeに「一般的なGoプロジェクトではどうやっていますか?」みたいな質問をして、なるべく郷に従うようにプロジェクトを構成しました。
書いてもらったコードでよく分からないところも都度質問しています。しょうもないことでも何度も聞けるのは助かります。

ちょっとずつ移行する

全ページを一度にどーんと書き直すのはだいぶしんどいので、エンドポイントごとにちょっとずつ移行することにしました。

ちょっとずつ移行するにあたって一番不安な要素は認証まわりでした。
Rails版でログインするときに使用するパスワードやセッションをGo版でも読んだり生成できる必要があります。
なので最初はログインやユーザー登録まわりから手をつけることにしました。

同じドメインでRailsとGoのサーバーを動かす

最初は go.annict.com という感じでサブドメインでGo版を動かすつもりでした。
しかしそうするとリダイレクトが必要になったり、ドメインが重要になる機能 (Passkeysなど) が提供しづらくなる恐れがありました。
そのためRails版もGo版も annict.com で動かすことにしました。

Goでリバースプロキシを実装し、全てのリクエストをGoで受けるようにした

同じドメインでRailsもGoも動くようにしたかったので、前段にリバースプロキシを置いてRailsかGoにリクエストを振り分ける必要がありました。

NginxやCaddyなどを別サービスで立ち上げて振り分けることも考えました。
しかし最終的には全てのリクエストをGoで処理することになるプロジェクトなので、Go側にリバースプロキシを実装し、そこで受けることにしました。

リバースプロキシはGoのアプリサーバーのミドルウェアとして実装したので、最終的に全ての処理をGoで行えるようになったらミドルウェアを削除するだけで済むはずです。

モノレポにする

最初はRails版のリポジトリとは別にGo版のリポジトリを作りそこで作業していましたが、以下の理由でモノレポにすることにしました。

  1. Goのコードを書いてもらうとき、Rails版のコードを参照しやすくなるため
  2. 同じドメインでRailsとGoのサーバーを動かすとき、開発環境が作りやすかったため

DockerとDocker Composeを使って開発しています。

1はRails版のリポジトリのディレクトリをGoのコンテナでマウントすれば別リポジトリでも問題なかったです。
ただ、モノレポにするとマウントなどの設定が不要という点は便利でした。

2はデータベースなどはRailsもGoも同じものを参照することになるので、Docker Composeでサービス名を指定するだけで接続できるようになり楽でした。

インフラ構成

以下のような構成で運用しています。

基本的にはDokkuでGo版のアプリケーションを作ることになったところ以外はRailsだけで運用していたころと変わらないです。

  • Cloudflare (CDN)
  • VPS (Ubuntu)
    • Dokku
      • Go
      • Rails
      • PostgreSQL
      • Redis
      • imgproxy (画像変換)
      • Vector (ログをR2に流す)
    • Mackerel (サーバー監視)
  • Cloudflare R2 (オブジェクトストレージ)
  • Resend (メール送信)
  • Sentry (エラー管理)

Go版のプロジェクト構成

簡易的なレイヤードアーキテクチャーみたいな構成に落ち着いています。

┌─────────────────────────────────────────────────────────┐
│ Presentation層                                          │
│ - Handler, ViewModel, Template, Middleware            │
└─────────────────────────────────────────────────────────┘
         ↓ 依存
┌─────────────────────────────────────────────────────────┐
│ Application層                                           │
│ - UseCase(ビジネスフロー、トランザクション管理)           │
└─────────────────────────────────────────────────────────┘
         ↓ 依存
┌─────────────────────────────────────────────────────────┐
│ Domain/Infrastructure層(統合)                          │
│ - Query (sqlc), Repository, Model                     │
└─────────────────────────────────────────────────────────┘

これは以前関西Ruby会議で紹介したものとほとんど同じになります。

詳細: 関西Ruby会議08に参加して発表しました

Rails版ではActiveRecordなクラス群であるRecordが至るところから参照されるのを許していました。
RailsはActiveRecordをつかってなんぼだと思うのでこうしていました。

しかしGo版ではデータベースとやり取りするQueryはRepositoryからのみ参照できるようにしています。
また、ViewModelを定義するようにしています。
これによってGo版はQuery (Record) とRepositoryの責務がより明確になったかなと思います。

Go版で使っているライブラリ

主要なものをご紹介します。

ルーティング: chi

https://go-chi.io/

net/http互換でシンプルそうだったので選びました。

データベースへの参照: sqlc

https://sqlc.dev/

SQLを自分で書くのは大変だと思っていましたが、最近はAIが書いてくれるようになりました。そのためORMを使わずSQLを直書きで良いのでは?と思い始めていました。
ORMを使っても結局どんなSQLが実行されているかは確認する必要があるので、それなら最初からSQLが直書きされているほうが直感的で読みやすいだろうという考えです。

sqlcはSQLからコードを生成してくれるので、型安全かつどんなSQLが実行されるのか意識しやすくて良いです。

テンプレートエンジン: templ

https://templ.guide/

Railsでビューを実装するときはView Componentを使うのが好きでした。
普通のRubyのコードのようにメソッドにデータを渡せるところと、ERBでHTMLが書けるところが好きでした。

JavaScript界隈で使われているJSXも好きです。
最終的な出力結果となるHTMLに限りなく近いフォーマットでテンプレートを書きたいという気持ちがあるんだと思います。

templからはGo + JSXみたいな印象を受けました。
データはGoの関数の引数として型付きで渡すことができ、テンプレート部分はJSXみたいなフォーマットで書くことができます。

型を意識してHTMLに近い形でテンプレートが書けるのが好みです。

バックグラウンドジョブ: River

https://riverqueue.com/

Rails版ではDelayedJobを使っています。
RiverもDelayedJobと同じくPostgreSQLで動くジョブサーバーになっています。
サーバーの構成をシンプルにするために、PostgreSQLでできることはPostgreSQLでやると良いかなと思うので、Riverの存在はありがたいです。

リンター: golangci-lint

https://golangci-lint.run/

いくつか設定していますが、中でも一番新鮮・導入できて嬉しかったのがDepguardというリンターです。

https://github.com/OpenPeeDeeP/depguard

これは各パッケージでどんなパッケージのインポートを許可・拒否するかといった設定ができるリンターです。

Go版は上に書いたように簡易的なレイヤードアーキテクチャーをもとにパッケージを構成しているため、例えば「TemplateはRepositoryに依存しないこと」といったルールが存在します。
このルールはドキュメントに書いてあるだけだったのでスルーすることもできる状態だったのですが、Depguardを導入して以下のようなルールを追加することで、機械的に依存関係がチェックできるようになりました。

.golangci.yml の設定例:

depguard:
  rules:
    # Templates層のルール: ViewModelを通じてデータを表示
    # 直接のデータアクセスとビジネスロジックに依存しない
    templates-layer:
      files:
        - "**/internal/templates/**/*.go"
      deny:
        - pkg: github.com/annict/annict/internal/query
          desc: "TemplatesはQueryに依存できません。"
        - pkg: github.com/annict/annict/internal/repository
          desc: "TemplatesはRepositoryに依存できません。"
        - pkg: github.com/annict/annict/internal/model
          desc: "TemplatesはModelに直接依存できません。ViewModelを経由してください。"
        - pkg: github.com/annict/annict/internal/usecase
          desc: "TemplatesはUseCaseに依存できません。"
        - pkg: github.com/annict/annict/internal/handler
          desc: "TemplatesはHandlerに依存できません。"
        - pkg: github.com/annict/annict/internal/middleware
          desc: "TemplatesはMiddlewareに依存できません。"

最低限の依存関係の強制ができれば良いので、ブラックリスト方式にして必ず守りたいことだけを記述することにしています。

Goに移行して良かったところ

型を意識した読みやすいコードで開発することができるようになりました。

開発しているときのCIの実行時間も、ビルドが速いおかげでRailsで開発しているときとあまり変わらず、早めのフィードバックループで開発できています。

メモリ使用量についても、Dokku上で起動しているRailsのwebプロセスが約800MB (マスターとワーカー2つの単純合計)、Goのwebプロセスが約30MBとなっています。
ただし現時点ではGo版は認証周りなど一部の機能しか実装していないため、単純な比較はできません。
今後機能が増えればGo版のメモリ使用量も増えていくかもしれませんが、それでもVPS1台での運用を続けるという方針において、リソース効率の良さは期待できそうです。

移行して不安なところ

Goとかの問題ではなく、自分のAIの使い方に漠然とした不安があります。

RailsプロジェクトでAIにコードを書いてもらうときは、僕がRailsに慣れているのもあり、AIが書いたコードをコントロールできている自信がありました。
アイアンマンのパワードスーツとかジャーヴィスみたいな感じで、自分の動きを強化してくれる感覚がありました。

一方Goはまだ自分が慣れていないことから、AIがコードをコントロールしている感じがしています。
ヒカルの碁の初期みたいな感じで、佐為が言った場所によくわからず碁石を置いている感覚です。

ただ、AIが書いたコードを一つ一つ読み、書籍を参照したり都度質問したりして理解するようにしているので、徐々に自分のものになってきている感覚はあります。
AIに書いてもらったコードなどを読んで勉強することで、後半のヒカルのように自立できたら良いなと思います。

今後について

まだログインやユーザー登録周りしか手をつけていないので、他のエンドポイントも移行していこうと思います。

環境構築からデプロイまでのフローはわりと整ったので、この知見をもとに MewstWikino といったAnnict以外のプロジェクトもGoに移行していこうと思っています。
CLAUDE.md を別リポジトリにコピペして、Annictと並行して作業していこうかなと思います。
また、最初からGoを使う新規サービスの開発も始めています。

新しい技術スタックで開発していくのが楽しみです。