Echo

高性能でミニマルなGo Webフレームワーク。より多くの組み込み機能を提供し、大規模アプリケーションに適している。

GoフレームワークWeb開発REST APIミニマリスト高性能

GitHub概要

labstack/echo

High performance, minimalist Go web framework

スター31,267
ウォッチ530
フォーク2,276
作成日:2015年3月1日
言語:Go
ライセンス:MIT License

トピックス

echogohttp2httpslabstack-echoletsencryptmicro-frameworkmicroservicemiddlewaresslwebweb-frameworkwebsocket

スター履歴

labstack/echo Star History
データ取得日時: 2025/7/16 08:43

フレームワーク

Echo

概要

Echoは、Go言語で書かれた高性能でミニマリストなWebフレームワークです。RESTful APIとWebアプリケーションの構築に特化しています。

詳細

Echo(エコー)は2015年にLabStack社によって開発された、シンプルさとパフォーマンスを重視したGoのWebフレームワークです。最適化されたHTTPルーター、豊富なミドルウェアエコシステム、そして直感的なAPIにより、開発者が高性能なWebアプリケーションを迅速に構築できるよう設計されています。ツリーベースのルーティングアルゴリズムを採用し、効率的なルートマッチングを実現しています。ミドルウェアシステムは階層化されており、グローバル、グループ、ルートレベルでの適用が可能です。JSON、XML、フォームデータの自動バインディング、集中型エラーハンドリング、HTTP/2サポート、Let's Encryptによる自動TLS機能を標準搭載しています。軽量でありながら企業レベルの機能を提供し、マイクロサービスアーキテクチャやAPIファーストな開発に最適です。

メリット・デメリット

メリット

  • 高いパフォーマンス: 最適化されたルーターと軽量設計による高速レスポンス
  • シンプルな学習コーブ: 直感的なAPIと分かりやすいドキュメント
  • 豊富なミドルウェア: ロギング、認証、CORS、圧縮など標準搭載
  • 柔軟なルーティング: パラメータ、ワイルドカード、グループ化対応
  • 自動バインディング: JSONやフォームデータの自動変換機能
  • HTTP/2対応: モダンなHTTPプロトコルサポート
  • コミュニティ活発: アクティブな開発とコミュニティサポート

デメリット

  • 機能の制約: フルスタックフレームワークに比べて機能が限定的
  • Go言語依存: Go特有の学習コストと開発環境の制約
  • ORM非搭載: データベース操作は別途ライブラリが必要
  • テンプレート制限: 組み込みテンプレートエンジンの機能が基本的
  • エコシステム: フレームワーク固有のツール群が比較的少ない

主要リンク

書き方の例

Hello World

package main

import (
	"net/http"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	// Echoインスタンスを作成
	e := echo.New()

	// ミドルウェア
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// ルート
	e.GET("/", hello)

	// サーバー起動
	e.Logger.Fatal(e.Start(":1323"))
}

// ハンドラー関数
func hello(c echo.Context) error {
	return c.String(http.StatusOK, "Hello, World!")
}

ルーティングとパラメータ

package main

import (
	"net/http"
	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()

	// 基本ルート
	e.GET("/", home)
	e.POST("/users", createUser)
	e.GET("/users/:id", getUser)
	e.PUT("/users/:id", updateUser)
	e.DELETE("/users/:id", deleteUser)

	// ワイルドカードルート
	e.GET("/files/*", getFile)

	// クエリパラメータ
	e.GET("/search", search)

	e.Logger.Fatal(e.Start(":1323"))
}

func home(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{
		"message": "Welcome to Echo API",
	})
}

func getUser(c echo.Context) error {
	// パスパラメータの取得
	id := c.Param("id")
	return c.JSON(http.StatusOK, map[string]string{
		"id": id,
		"name": "ユーザー " + id,
	})
}

func search(c echo.Context) error {
	// クエリパラメータの取得
	query := c.QueryParam("q")
	page := c.QueryParam("page")
	
	if query == "" {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "検索クエリが必要です",
		})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"query": query,
		"page": page,
		"results": []string{"結果1", "結果2", "結果3"},
	})
}

func getFile(c echo.Context) error {
	// ワイルドカードパラメータの取得
	filepath := c.Param("*")
	return c.JSON(http.StatusOK, map[string]string{
		"filepath": filepath,
	})
}

JSONデータバインディングとバリデーション

package main

import (
	"net/http"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/go-playground/validator/v10"
)

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name" validate:"required,min=2,max=50"`
	Email string `json:"email" validate:"required,email"`
	Age   int    `json:"age" validate:"min=0,max=120"`
}

type CustomValidator struct {
	validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
	return cv.validator.Struct(i)
}

func main() {
	e := echo.New()

	// カスタムバリデーターを設定
	e.Validator = &CustomValidator{validator: validator.New()}

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.POST("/users", createUser)
	e.GET("/users/:id", getUser)

	e.Logger.Fatal(e.Start(":1323"))
}

func createUser(c echo.Context) error {
	user := new(User)

	// JSONデータをバインド
	if err := c.Bind(user); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "Invalid JSON format",
		})
	}

	// バリデーション
	if err := c.Validate(user); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": err.Error(),
		})
	}

	// ユーザー作成処理(実際のDBへの保存など)
	user.ID = 123 // 仮のID

	return c.JSON(http.StatusCreated, user)
}

func getUser(c echo.Context) error {
	// 仮のユーザーデータ
	user := User{
		ID:    1,
		Name:  "山田太郎",
		Email: "[email protected]",
		Age:   30,
	}

	return c.JSON(http.StatusOK, user)
}

ミドルウェアの使用

package main

import (
	"net/http"
	"time"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	e := echo.New()

	// グローバルミドルウェア
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.CORS())

	// レート制限
	e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))

	// セキュアヘッダー
	e.Use(middleware.Secure())

	// Gzip圧縮
	e.Use(middleware.Gzip())

	// リクエストタイムアウト
	e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
		Timeout: 30 * time.Second,
	}))

	// パブリックルート
	e.GET("/", publicHandler)
	e.POST("/register", registerHandler)

	// 認証が必要なAPIグループ
	api := e.Group("/api")
	api.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
		if username == "admin" && password == "secret" {
			return true, nil
		}
		return false, nil
	}))

	api.GET("/users", listUsers)
	api.POST("/users", createUser)

	// カスタムミドルウェア
	e.Use(customLogger)

	e.Logger.Fatal(e.Start(":1323"))
}

func customLogger(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		start := time.Now()
		
		// 次のハンドラーを実行
		err := next(c)
		
		// ログ出力
		c.Logger().Infof("Method: %s, URI: %s, Status: %d, Latency: %v",
			c.Request().Method,
			c.Request().RequestURI,
			c.Response().Status,
			time.Since(start),
		)
		
		return err
	}
}

func publicHandler(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{
		"message": "パブリックエンドポイント",
	})
}

func registerHandler(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{
		"message": "ユーザー登録",
	})
}

func listUsers(c echo.Context) error {
	return c.JSON(http.StatusOK, []map[string]interface{}{
		{"id": 1, "name": "ユーザー1"},
		{"id": 2, "name": "ユーザー2"},
	})
}

エラーハンドリング

package main

import (
	"errors"
	"net/http"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type HTTPError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

func main() {
	e := echo.New()

	// カスタムエラーハンドラー
	e.HTTPErrorHandler = customHTTPErrorHandler

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/", successHandler)
	e.GET("/error", errorHandler)
	e.GET("/panic", panicHandler)
	e.GET("/users/:id", getUserWithError)

	e.Logger.Fatal(e.Start(":1323"))
}

func customHTTPErrorHandler(err error, c echo.Context) {
	var code int
	var message string

	if he, ok := err.(*echo.HTTPError); ok {
		code = he.Code
		message = he.Message.(string)
	} else {
		code = http.StatusInternalServerError
		message = "Internal Server Error"
	}

	// エラーログ出力
	c.Logger().Error(err)

	// JSONエラーレスポンス
	if !c.Response().Committed {
		if c.Request().Method == http.MethodHead {
			err = c.NoContent(code)
		} else {
			err = c.JSON(code, HTTPError{
				Code:    code,
				Message: message,
			})
		}
		if err != nil {
			c.Logger().Error(err)
		}
	}
}

func successHandler(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{
		"message": "成功",
	})
}

func errorHandler(c echo.Context) error {
	return echo.NewHTTPError(http.StatusBadRequest, "カスタムエラーメッセージ")
}

func panicHandler(c echo.Context) error {
	panic("パニックが発生しました")
}

func getUserWithError(c echo.Context) error {
	id := c.Param("id")
	
	// ビジネスロジックエラー
	if id == "999" {
		return echo.NewHTTPError(http.StatusNotFound, "ユーザーが見つかりません")
	}

	// システムエラー
	if id == "500" {
		return errors.New("データベース接続エラー")
	}

	return c.JSON(http.StatusOK, map[string]string{
		"id": id,
		"name": "ユーザー " + id,
	})
}

ファイルアップロードと静的ファイル配信

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// 静的ファイル配信
	e.Static("/static", "assets")

	// ファイルアップロード
	e.POST("/upload", uploadFile)
	e.POST("/upload/multiple", uploadMultipleFiles)

	// ファイルダウンロード
	e.GET("/download/:filename", downloadFile)

	e.Logger.Fatal(e.Start(":1323"))
}

func uploadFile(c echo.Context) error {
	// フォームからファイルを取得
	file, err := c.FormFile("file")
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "ファイルの取得に失敗しました")
	}

	// ファイルサイズチェック(10MB制限)
	if file.Size > 10<<20 {
		return echo.NewHTTPError(http.StatusBadRequest, "ファイルサイズが大きすぎます(最大10MB)")
	}

	// ファイルを開く
	src, err := file.Open()
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "ファイルの読み込みに失敗しました")
	}
	defer src.Close()

	// アップロードディレクトリが存在しない場合は作成
	uploadDir := "uploads"
	if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
		err = os.Mkdir(uploadDir, 0755)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "アップロードディレクトリの作成に失敗しました")
		}
	}

	// 保存先ファイルを作成
	dst, err := os.Create(filepath.Join(uploadDir, file.Filename))
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "ファイルの作成に失敗しました")
	}
	defer dst.Close()

	// ファイルをコピー
	if _, err = io.Copy(dst, src); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "ファイルの保存に失敗しました")
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"message":  "ファイルアップロード成功",
		"filename": file.Filename,
		"size":     file.Size,
	})
}

func uploadMultipleFiles(c echo.Context) error {
	// マルチパートフォームを解析
	form, err := c.MultipartForm()
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "マルチパートフォームの解析に失敗しました")
	}

	files := form.File["files"]
	var uploadedFiles []string

	for _, file := range files {
		// ファイルサイズチェック
		if file.Size > 10<<20 {
			continue // 大きすぎるファイルはスキップ
		}

		src, err := file.Open()
		if err != nil {
			continue
		}

		dst, err := os.Create(filepath.Join("uploads", file.Filename))
		if err != nil {
			src.Close()
			continue
		}

		if _, err = io.Copy(dst, src); err != nil {
			src.Close()
			dst.Close()
			continue
		}

		src.Close()
		dst.Close()
		uploadedFiles = append(uploadedFiles, file.Filename)
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"message":        "ファイルアップロード完了",
		"uploaded_files": uploadedFiles,
		"count":          len(uploadedFiles),
	})
}

func downloadFile(c echo.Context) error {
	filename := c.Param("filename")
	filePath := filepath.Join("uploads", filename)

	// ファイルの存在チェック
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		return echo.NewHTTPError(http.StatusNotFound, "ファイルが見つかりません")
	}

	// Content-Dispositionヘッダーを設定してダウンロードを促す
	c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))

	return c.File(filePath)
}