PHPerに捧ぐ Go 人気Webフレームワーク実装で比較 2024 Gin Fiber Echo Chi
はじめに
PHPerの皆さん、お仕事お疲れ様です!
日々のWeb開発、PHPでゴリゴリ書いてますか? たまには気分転換に別の言語で開発してみませんか?
そんなあなたに、ぜひ試していただきたいのがGo言語です。Goは、シンプルさ、高速性、並行処理の強力さを兼ね備えた言語で、Webアプリケーション開発にも最適です。PHPで培ったWeb開発の知識を活かしつつ、Goの世界に飛び込んでみませんか?
今回は、私のようなPHPのフレームワークに慣れ親しんだ方々のために、Goの主要なWebフレームワークであるGin、Echo、Fiber、Chiを、実際にTodo APIを実装しながら比較し、それぞれの特徴を分かりやすく解説します。
実装するTodo API
今回の比較では、以下のシンプルなTodo APIを実装します。
- GET /todos: 全てのTodoを取得
- GET /todos/:id: 指定したIDのTodoを取得
- POST /todos: 新しいTodoを作成
実装内容は、ルーティング定義、ミドルウェア、リクエストハンドラ、そしてテストコードまでを含みます。
実装したコードはこちらのリポジトリで公開していますので、ぜひ参考にしてください。
記事にコードを掲載する都合上、若干コードの内容が違いますが、ご容赦ください。
プロジェクト構成
以下はプロジェクトの構造ツリーとなります。
.
├── main.go
├── chiapp
│ ├── routes.go # API実装
│ └── routes_test.go # テストコード
├── echoapp
│ ├── routes.go
│ └── routes_test.go
├── fiberapp
│ ├── routes.go
│ └── routes_test.go
├── ginapp
│ ├── routes.go
│ └── routes_test.go
├── repository
│ └── todos.go # 永続化共通処理
└── shared
├── server.go
└── token.go # トークン検証共通処理
各フレームワークごとにAPI実装とテストコードをchiapp
,echoapp
,fiberapp
,ginapp
に配置しています。
repository
やshared
については、各ミドルウェアやリクエストハンドラで実行する共通処理などを配置しています。
準備
実装を開始する前に、プロジェクトの初期設定や共通処理の実装を行います。
プロジェクト初期設定
以下のコマンドでプロジェクトを初期化します。
# プロジェクト名はappとしています
go mod init app
パッケージ追加
テストコード共通で利用するパッケージを追加します。
go get github.com/stretchr/testify
stretchr/testify
は、Goの標準ライブラリのtesting
パッケージを拡張した、テストをより簡単に、より読みやすくするためのパッケージです
共通処理実装
リクエストハンドラで利用する永続化やトークン検証などの共通処理を実装します。
1. 永続化処理
TodoをInMemoryで永続化する処理を実装しています。
2. トークン検証処理
トークンの検証処理を実装しています。
トークンはクエリパラメータにtoken
を指定してリクエストするような、ゆるい仕様を想定しています。
準備が整いました。
いよいよ各フレームワークごとに実装を進めていきましょう!
Gin
Ginは、GoのWebフレームワークの中でも最も人気のある老舗フレームワークで、PHPで言うとLaravelやSymfonyくらいの知名度があります。日本語の情報やGithubでの実装サンプルも多いので、Goの初学者に最もおすすめです。
特徴
- 高速なパフォーマンス: httprouter をベースにしたルーティングエンジンにより、高速なリクエスト処理を実現しています。
- 軽量: 必要最低限の機能のみを提供することで、フレームワーク自体が軽量で、メモリ使用量も抑えられます。
- ミドルウェア: 多くの組み込みミドルウェアが提供されており、認証、ロギング、エラー処理などを簡単に実装できます。
実装
1. パッケージ追加
go get -u github.com/gin-gonic/gin
2. ルーティング定義
package ginapp
// import文は省略
func SetupRoutes(router *gin.Engine) {
// api ルートのグループ
v1 := router.Group("/api/v1")
{
// クエリパラメーターによるトークン認証
v1.Use(tokenAuth)
// todo ルートのグループ
todo := v1.Group("/todos")
{
// 一覧取得
todo.GET("", getTodos)
// 詳細取得
todo.GET("/:id", getTodoById)
// 作成
todo.POST("", createTodo)
}
}
}
3. ミドルウェア/リクエストハンドラ
func tokenAuth(ctx *gin.Context) {
token := ctx.Query("token")
// トークンが一致しない場合は 401 を返す
if shared.IsInvalidToken(token) {
ctx.JSON(http.StatusUnauthorized, gin.H{
"message": "Unauthorized",
})
ctx.Abort()
}
ctx.Next()
}
func getTodoById(ctx *gin.Context) {
// パスパラメーターから id を取得
id := ctx.Param("id")
todo, ok := repository.GetTodoById(id)
if !ok {
ctx.JSON(http.StatusNotFound, gin.H{
"message": "Not Found",
})
}
ctx.JSON(http.StatusOK, gin.H{
"todo": todo,
})
}
func createTodo(ctx *gin.Context) {
todo := &repository.TodoForCreate{}
// リクエストボディをパース
ctx.BindJSON(todo)
created := repository.CreateTodo(*todo)
ctx.JSON(http.StatusCreated, gin.H{
"todo": created,
})
}
4. テストコード
package ginapp
// import文は省略
func TestRoutes(t *testing.T) {
engine := gin.Default()
SetupRoutes(engine)
t.Run("無効なトークンのテスト", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/todos?token=invalid", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, 401, w.Code)
})
// 省略...
t.Run("Todo作成のテスト", func(t *testing.T) {
todo := repository.TodoForCreate{
Title: "test",
Completed: false,
}
data, _ := json.Marshal(todo)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/todos?token="+shared.Token, strings.NewReader(string(data)))
req.Header.Set("Content-Type", "application/json")
engine.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
// レスポンスの body をパースして Todo を取得
var body struct {
Todo repository.Todo `json:"todo"`
}
json.NewDecoder(w.Body).Decode(&body)
assert.Equal(t, todo.Title, body.Todo.Title)
assert.Equal(t, todo.Completed, body.Todo.Completed)
})
}
5. サーバー起動
package main
import (
"app/ginapp"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
ginapp.SetupRoutes(engine)
log.Fatal(engine.Run(":8080"))
// go run main.go で実行して``curl -X GET "http://localhost:8080/api/v1/todos?token=token"``でアクセスできます
}
感想
さすが一番人気のWebフレームワークだけあって、非常にシンプルで使いやすかったです。情報量が多いので、実装方法を調べるのに困ることもありません。Go初心者の方には、Ginを利用するのがおすすめです。
Fiber
Fiberは、GitHub のスター数3万を超える、GoのWebフレームワークの中でも注目度の高いフレームワークです。Goで最も高速なフレームワークの1つであり、ベンチマークテストでは、GinやEchoなどの他のGoフレームワークを凌駕するパフォーマンスを示しているらしいです。
今回はせっかくなので、Fiberの最新バージョンv3(beta)を使用して実装していきます。
特徴
- 高速なパフォーマンス: Fasthttpをベースに構築されており、Goで最も高速なフレームワークの1つです。ベンチマークテストでは、GinやEchoなどの他のGoフレームワークを凌駕するパフォーマンスを示しているようです。
- 軽量: メモリ使用量も非常に少なく、リソースに制約のある環境でも効率的に動作します。
- Express.jsライク: Node.jsのExpressに似たAPIを提供し、Expressユーザーにとって馴染みやすいらしいです。
実装
1. パッケージ追加
go get -u github.com/gofiber/fiber/v3
Fiber v3 はまだベータ版なので、実務ではv2を使うのが無難です
2. ルーティング定義
package fiberapp
// import文は省略
func SetupRoutes(router *fiber.App) {
// api ルートのグループ
v1 := router.Group("/api/v1").Name("api.")
{
// クエリパラメーターによるトークン認証
v1.Use(tokenAuth)
// todo ルートのグループ
todo := v1.Group("/todos").Name("todos.")
{
// 一覧取得
todo.Get("", getTodos).Name("index") // ルートに名付けできる!便利ー!
// 詳細取得
todo.Get("/:id", getTodoById).Name("show")
// 作成
todo.Post("", createTodo).Name("store")
}
}
// 名付けしたルートは``router.GetRoute("api.todos.index")``で参照できる!
}
3. ミドルウェア/リクエストハンドラ
func tokenAuth(ctx fiber.Ctx) error {
// Fiberのv3ではContextがインタフェースで提供されている
token := fiber.Query[string](ctx, "token")
// トークンが一致しない場合は 401 を返す
if shared.IsInvalidToken(token) {
return ctx.Status(http.StatusUnauthorized).JSON(fiber.Map{
"message": "Unauthorized",
})
}
return ctx.Next()
}
func getTodos(ctx fiber.Ctx) error {
todos := repository.GetTodos()
return ctx.JSON(fiber.Map{
"todos": todos,
})
}
func getTodoById(ctx fiber.Ctx) error {
// パスパラメーターをジェネリクスで型指定して取得できる
id := fiber.Params[string](ctx, "id")
todo, ok := repository.GetTodoById(id)
if !ok {
return ctx.Status(http.StatusNotFound).JSON(fiber.Map{
"message": "Not Found",
})
}
return ctx.JSON(fiber.Map{
"todo": todo,
})
}
func createTodo(ctx fiber.Ctx) error {
todo := &repository.TodoForCreate{}
// リクエストボディをパース
// v2 の場合は``if err := ctx.BodyParser(&todo); err != nil {``となるので注意!
if err := ctx.Bind().Body(todo); err != nil {
return err
}
created := repository.CreateTodo(*todo)
return ctx.Status(http.StatusCreated).JSON(fiber.Map{
"todo": created,
})
}
4. テストコード
package fiberapp
// import文は省略
func TestRoutes(t *testing.T) {
app := fiber.New()
SetupRoutes(app)
t.Run("無効なトークンのテスト", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/v1/todos?token=invalid", nil)
// FiberではTestメソッドでリクエストを送信できる
res, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, res.StatusCode)
})
// 省略...
t.Run("Todo作成のテスト", func(t *testing.T) {
todo := repository.TodoForCreate{
Title: "test",
Completed: false,
}
data, _ := json.Marshal(todo)
req, _ := http.NewRequest("POST", "/api/v1/todos?token="+shared.Token, strings.NewReader(string(data)))
req.Header.Set("Content-Type", "application/json")
res, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 201, res.StatusCode)
// レスポンスの body をパースして Todo を取得
var body struct {
Todo repository.Todo `json:"todo"`
}
json.NewDecoder(res.Body).Decode(&body)
assert.Equal(t, todo.Title, body.Todo.Title)
assert.Equal(t, todo.Completed, body.Todo.Completed)
})
}
5. サーバー起動
package main
import (
"app/fiberapp"
"github.com/gofiber/fiber/v3"
)
func main() {
app := fiber.New()
fiberapp.SetupRoutes(app)
log.Fatal(app.Listen(":8080"))
// go run main.go で実行して``curl -X GET "http://localhost:8080/api/v1/todos?token=token"``でアクセスできます
}
感想
Fiberは、Go言語の文法を活かした設計で、非常に開発しやすいフレームワークだと感じました。公式ドキュメントも充実しており、初心者でも安心して利用できます。v3から導入されたジェネリクスによるルートやクエリパラメータの型指定は、コードの安全性を高める上で非常に有効です。Fiberは、パフォーマンスと開発効率の両方を求める開発者におすすめです。
Echo
Echoは、ミニマリストな設計思想を持ち、高いパフォーマンスと柔軟性を両立させているフレームワークです。GoのWebフレームワークといえばGinかEchoと言われることが多いほど、人気と実績のあるフレームワークです。
特徴
- 高速なパフォーマンス: Radix tree をベースにしたルーティングエンジンにより、高速なリクエスト処理を実現しています。Ginと同様に、ベンチマークテストでも優れたパフォーマンスを示しており、速度を重視する開発者から支持されています。
- 拡張性/カスタマイズ性: ルーティングやミドルウェアの設定を細かく拡張&カスタマイズできるため、多様なニーズに対応できます。
実装
1. パッケージ追加
go get -u github.com/labstack/echo/v4
2. ルーティング定義
package echoapp
// import は省略
// ルーティングのセットアップ
func SetupRoutes(router *echo.Echo) {
// api ルートのグループ
v1 := router.Group("/api/v1")
{
// クエリパラメーターによる認証
v1.Use(tokenAuth)
// todo ルートのグループ
todo := v1.Group("/todos")
{
// 一覧取得
todo.GET("", getTodos).Name = "api.todos.index" // ルートに名付けできるけどグループに名前をつけることはできない
// 詳細取得
todo.GET("/:id", getTodoById).Name = "api.todos.show"
// 作成
todo.POST("", createTodo).Name = "api.todos.store"
}
}
}
3. ミドルウェア/リクエストハンドラ
func tokenAuth(next echo.HandlerFunc) echo.HandlerFunc {
// EchoではContextがインタフェースで提供されている
return func(ctx echo.Context) error {
token := ctx.QueryParam("token")
// トークンが一致しない場合は 401 を返す
if shared.IsInvalidToken(token) {
return ctx.JSON(http.StatusUnauthorized, echo.Map{
"message": "Unauthorized",
})
}
return next(ctx)
}
}
func getTodos(ctx echo.Context) error {
todos := repository.GetTodos()
return ctx.JSON(http.StatusOK, echo.Map{
"todos": todos,
})
}
func getTodoById(ctx echo.Context) error {
// パスパラメーターから id を取得
id := ctx.Param("id")
todo, ok := repository.GetTodoById(id)
if !ok {
return ctx.JSON(http.StatusNotFound, echo.Map{
"message": "Not Found",
})
}
return ctx.JSON(http.StatusOK, echo.Map{
"todo": todo,
})
}
func createTodo(ctx echo.Context) error {
todo := &repository.TodoForCreate{}
// リクエストボディをパース
if err := ctx.Bind(todo); err != nil {
return ctx.JSON(http.StatusBadRequest, echo.Map{
"message": "Bad Request",
})
}
created := repository.CreateTodo(*todo)
return ctx.JSON(http.StatusCreated, echo.Map{
"todo": created,
})
}
4. テストコード
package echoapp
// import は省略
func TestRoutes(t *testing.T) {
app := echo.New()
SetupRoutes(app)
t.Run("無効なトークンのテスト", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/todos?token=invalid", nil)
app.ServeHTTP(w, req)
assert.Equal(t, 401, w.Code)
})
// 省略...
t.Run("Todo作成のテスト", func(t *testing.T) {
todo := repository.TodoForCreate{
Title: "test",
Completed: false,
}
data, _ := json.Marshal(todo)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/todos?token="+shared.Token, strings.NewReader(string(data)))
req.Header.Set("Content-Type", "application/json")
app.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
// レスポンスの body をパースして Todo を取得
var body struct {
Todo repository.Todo `json:"todo"`
}
json.NewDecoder(w.Body).Decode(&body)
assert.Equal(t, todo.Title, body.Todo.Title)
assert.Equal(t, todo.Completed, body.Todo.Completed)
})
}
5. サーバー起動
package main
import (
"app/echoapp"
"github.com/labstack/echo/v4"
)
func main() {
app := echo.New()
echoapp.SetupRoutes(app)
app.Logger.Fatal(app.Start(":8080"))
// go run main.go で実行して``curl -X GET "http://localhost:8080/api/v1/todos?token=token"``でアクセスできます
}
感想
Echoは、ミニマルな設計でありながら、必要な機能はしっかりと揃っている、バランスの取れたフレームワークだと感じました。PHP のフレームワークで言うとSlimやCodeIgniterのような感じで、小規模なAPI サーバーを作るのに向いていると思います。
Chi
Chiはnet/http
標準ライブラリとの互換性を重視した、軽量でシンプルなWebフレームワークです。人気も高く、GitHubのスター数も1万を超えているので、安心して利用できます。
特徴
- 軽量: 必要最低限の機能のみを提供することで、フレームワーク自体が非常に軽量です。Goの標準ライブラリを最大限に活用し、依存関係を最小限に抑えることで、シンプルで高速なWebアプリケーションを構築できます。
-
柔軟性:
net/http
との互換性が高く、標準ライブラリのハンドラやミドルウェアをそのまま利用できます。 - 強力なルーティング: URLパラメータ、ワイルドカード、正規表現などを用いた柔軟なルーティングをサポートしています。
実装
1. パッケージ追加
go get -u github.com/go-chi/chi/v5
# 必要に応じてレスポンスをサポートするためのパッケージを一緒に追加します(今回は利用する)
go get github.com/go-chi/render
2. ルーティング定義
package chiapp
// import文は省略
func SetupRoutes(router *chi.Mux) {
// api ルートのグループ(サブルーター)化
router.Route("/api/v1", func(r chi.Router) {
// クエリパラメーターによる認証
r.Use(tokenAuth)
// todo ルートのグループ(サブルーター)化
r.Route("/todos", func(todo chi.Router) {
// 一覧取得
todo.Get("/", getTodos)
// 詳細取得 ルートパスに対して``/{slug:[a-z-]+}``のような正規表現で制約を指定することも可能
todo.Get("/{id}", getTodoById)
// 作成
todo.Post("/", createTodo)
})
})
}
3. ミドルウェア/リクエストハンドラ
// Jsonレスポンスの型
type ChiMap map[string]interface{}
// Todo作成リクエストの型
type CreateTodoRequest struct {
Title string `json:"title"`
Completed bool `json:"completed"`
}
// render.Bidnerインタフェースの実装
func (todo *CreateTodoRequest) Bind(r *http.Request) error {
return nil
}
func tokenAuth(next http.Handler) http.Handler {
// Contextがインタフェースで提供されている
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
// トークンが一致しない場合は 401 を返す
if shared.IsInvalidToken(token) {
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ChiMap{
"message": "Unauthorized",
})
}
next.ServeHTTP(w, r)
},
)
}
func getTodos(w http.ResponseWriter, r *http.Request) {
todos := repository.GetTodos()
render.JSON(w, r, ChiMap{
"todos": todos,
})
}
func getTodoById(w http.ResponseWriter, r *http.Request) {
// パスパラメーターから id を取得
id := chi.URLParam(r, "id")
todo, ok := repository.GetTodoById(id)
if !ok {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ChiMap{
"message": "Not Found",
})
}
render.JSON(w, r, ChiMap{
"todo": todo,
})
}
func createTodo(w http.ResponseWriter, r *http.Request) {
todo := &CreateTodoRequest{}
// リクエストボディをパース
if err := render.Bind(r, todo); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ChiMap{
"message": "Bad Request",
})
}
created := repository.CreateTodo(repository.TodoForCreate{
Title: todo.Title,
Completed: todo.Completed,
})
render.Status(r, http.StatusCreated)
render.JSON(w, r, ChiMap{
"todo": created,
})
}
4. テストコード
package chiapp
// import文は省略
func TestRoutes(t *testing.T) {
mux := chi.NewMux()
SetupRoutes(mux)
t.Run("無効なトークンのテスト", func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/todos?token=invalid", nil)
mux.ServeHTTP(w, req)
assert.Equal(t, 401, w.Code)
})
// 省略...
t.Run("Todo作成のテスト", func(t *testing.T) {
todo := repository.TodoForCreate{
Title: "test",
Completed: false,
}
data, _ := json.Marshal(todo)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/todos?token="+shared.Token, strings.NewReader(string(data)))
req.Header.Set("Content-Type", "application/json")
mux.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
// レスポンスの body をパースして Todo を取得
var body struct {
Todo repository.Todo `json:"todo"`
}
json.NewDecoder(w.Body).Decode(&body)
assert.Equal(t, todo.Title, body.Todo.Title)
assert.Equal(t, todo.Completed, body.Todo.Completed)
})
}
5. サーバー起動
package main
import (
"app/chiapp"
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
mux := chi.NewMux()
chiapp.SetupRoutes(mux)
addr := ":8080"
fmt.Printf("Server running on %s\n", addr)
log.Fatal(http.ListenAndServe(addr, mux))
// go run main.go で実行して``curl -X GET "http://localhost:8080/api/v1/todos?token=token"``でアクセスできます
}
感想
Chiは、net/http
標準ライブラリをそのまま利用するような感覚で使えるフレームワークです。そのため、標準ライブラリに慣れている開発者にとっては、非常に使いやすく感じるでしょう。しかし、他のフレームワークと比べると、提供される機能が少ないため、github.com/go-chi/render
などの外部パッケージを利用する必要があり、コード量が多くなる傾向があります。
比較してみて
フレームワーク | メリット | デメリット | どういう人向け? |
---|---|---|---|
Gin | 学習コストが低い、豊富なミドルウェア、情報量が多い | - | Go初心者、素早く API を開発したい人 |
Fiber | 超高速なパフォーマンス、Express.jsライクな API、豊富な機能、v3での機能強化 | コミュニティが比較的小さい | パフォーマンス重視、快適な実装体験を求める人 |
Echo | 高速なパフォーマンス、拡張性が高い、ミニマルな設計 | - | シンプルなフレームワークを好む人、ミニマリスト |
Chi | 標準ライブラリとの互換性が高い、柔軟性が高い | 機能が少ない、コード量が多くなる傾向がある | 標準ライブラリに慣れている人、バリバリカスタマイズしたい人 |
そしてHumaへ...
今回、GoのWebフレームワークを調べてると、Humaというフレームワークを見つけました!
記事でも紹介されているのですが...
なんとこのフレームワーク...
今回紹介したGin、Fiber、Echo、Chiなどのフレームワークをルーターとして利用できるという代物になっています!
次回は、Humaを使ってTodo APIを実装し、その魅力を紹介します。
以下は、次回記事です。
まとめ
今回は、Goの主要なWebフレームワークを4つ紹介し、それぞれの特徴を比較しました。どのフレームワークもそれぞれにメリット・デメリットがあり、最適なフレームワークは開発するアプリケーションや開発チームのスキルセットによって異なります。
この記事が、GoのWebフレームワーク選びの参考になれば幸いです。
Discussion