【Go】Echoを使ったAPIの説明について

Go

kamiです。
TwitterYoutubeもやってます。

今回はGo言語のEchoを使ったAPIの説明についての紹介です。

Echoとはとは?

Go の Web フレームワーク Echo は、シンプルで高速な API 開発を可能にするフレームワークです。

  • 小限のメモリ消費で、高速な処理が可能
  • RESTful API に適した直感的なルーティング
  • ログ記録、リカバリー、CORS など組み込みミドルウェアあり
  • JSON のリクエスト/レスポンス処理が直感的
  • WebSocket やサーバー送信イベント(SSE)をサポート

go get

goのv1.16以前までの機能として、go.modの編集とバイナリ(ライブラリ)のインストールでした。しかし、v1.16からgo installとgo getに機能が別れました。go installはバイナリ(ライブラリ)をインストールしgo getはgo.modを編集するだけの機能になりました。

スポンサードサーチ

go mod tidy

「go mod tidy」することで、「go.sum」ファイルが追加または、更新されます。

Echoを使ったmain.goの全体のコード

Echoを使った場合、「import “github.com/labstack/echo/v4″」が追加されます。
コードを追加した後は、「go run main.go」する前に「go mod tidy」をしましょう。

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

// データモデル
type (
	// User ユーザーモデル
	User struct {
		ID        int       `json:"id"`
		Name      string    `json:"name" form:"name" query:"name"`
		Email     string    `json:"email" form:"email" query:"email"`
		CreatedAt time.Time `json:"created_at"`
		UpdatedAt time.Time `json:"updated_at"`
	}

	// Product 製品モデル
	Product struct {
		ID          int     `json:"id"`
		Name        string  `json:"name" form:"name" query:"name"`
		Description string  `json:"description" form:"description" query:"description"`
		Price       float64 `json:"price" form:"price" query:"price"`
		CategoryID  int     `json:"category_id" form:"category_id" query:"category_id"`
	}

	// Category カテゴリモデル
	Category struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	}

	// SearchParams 検索パラメータ
	SearchParams struct {
		Query  string `query:"q"`
		Limit  int    `query:"limit"`
		Offset int    `query:"offset"`
	}

	// CustomResponse カスタムレスポンス
	CustomResponse struct {
		Status  int         `json:"status"`
		Message string      `json:"message"`
		Data    interface{} `json:"data,omitempty"`
	}

	// CustomContext カスタムコンテキスト
	CustomContext struct {
		echo.Context
		StartTime time.Time
	}
)

// 仮のデータストア
var (
	users      = map[int]User{}
	products   = map[int]Product{}
	categories = map[int]Category{}
	userID     = 1
	productID  = 1
	categoryID = 1
	uploadDir  = "uploads"
)

// カスタムHTTPエラーハンドラー
func customHTTPErrorHandler(err error, c echo.Context) {
	code := http.StatusInternalServerError
	message := "サーバーエラーが発生しました"

	if he, ok := err.(*echo.HTTPError); ok {
		code = he.Code
		message = fmt.Sprintf("%v", he.Message)
	}

	// リクエストがAccepts JSONの場合はJSONでエラーを返す
	if c.Request().Header.Get("Accept") == "application/json" {
		c.JSON(code, CustomResponse{
			Status:  code,
			Message: message,
		})
		return
	}

	// それ以外はHTMLでエラーを返す
	c.HTML(code, fmt.Sprintf("<h1>Error: %d</h1><p>%s</p>", code, message))
}

// カスタムコンテキストミドルウェア
func customContextMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		cc := &CustomContext{
			Context:   c,
			StartTime: time.Now(),
		}
		return next(cc)
	}
}

// レスポンスタイム計測ミドルウェア
func responseTimeMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		if cc, ok := c.(*CustomContext); ok {
			err := next(c)
			responseTime := time.Since(cc.StartTime)
			c.Response().Header().Set("X-Response-Time", responseTime.String())
			return err
		}
		return next(c)
	}
}

// メイン関数
func main() {
	// サーバーのセットアップ
	e := echo.New()
	e.HTTPErrorHandler = customHTTPErrorHandler

	// ミドルウェアの設定
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.CORS())
	e.Use(customContextMiddleware)
	e.Use(responseTimeMiddleware)

	// アップロードディレクトリの作成
	if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
		os.Mkdir(uploadDir, 0755)
	}

	// 初期データの作成
	createInitialData()

	// ルートグループ
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Welcome to Echo API Learning Server")
	})

	// API バージョン
	v1 := e.Group("/api/v1")

	// ユーザー関連エンドポイント
	users := v1.Group("/users")
	users.GET("", getAllUsers)
	users.GET("/:id", getUserById)
	users.POST("", createUser)
	users.PUT("/:id", updateUser)
	users.PATCH("/:id", partialUpdateUser)
	users.DELETE("/:id", deleteUser)
	users.GET("/search", searchUsers)

	// 製品関連エンドポイント
	products := v1.Group("/products")
	products.GET("", getAllProducts)
	products.GET("/:id", getProductById)
	products.POST("", createProduct)
	products.PUT("/:id", updateProduct)
	products.DELETE("/:id", deleteProduct)
	products.GET("/search", searchProducts)
	products.GET("/category/:category_id", getProductsByCategory)

	// カテゴリ関連エンドポイント
	categories := v1.Group("/categories")
	categories.GET("", getAllCategories)
	categories.GET("/:id", getCategoryById)
	categories.POST("", createCategory)

	// ファイルアップロード
	v1.POST("/upload", uploadFile)
	v1.GET("/files/:filename", getFile)

	// その他のエンドポイント
	v1.GET("/ping", pingHandler)
	v1.GET("/time", getServerTime)
	v1.GET("/headers", getRequestHeaders)
	v1.GET("/context", contextExample)
	v1.GET("/stream", streamResponse)

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

// 初期データの作成
func createInitialData() {
	// カテゴリの作成
	categories[categoryID] = Category{ID: categoryID, Name: "電子機器"}
	categoryID++
	categories[categoryID] = Category{ID: categoryID, Name: "家具"}
	categoryID++

	// 製品の作成
	products[productID] = Product{
		ID:          productID,
		Name:        "スマートフォン",
		Description: "最新のスマートフォン",
		Price:       89000,
		CategoryID:  1,
	}
	productID++
	products[productID] = Product{
		ID:          productID,
		Name:        "ラップトップ",
		Description: "高性能ノートパソコン",
		Price:       120000,
		CategoryID:  1,
	}
	productID++
	products[productID] = Product{
		ID:          productID,
		Name:        "デスク",
		Description: "木製デスク",
		Price:       25000,
		CategoryID:  2,
	}
	productID++

	// ユーザーの作成
	now := time.Now()
	users[userID] = User{
		ID:        userID,
		Name:      "田中太郎",
		Email:     "tanaka@example.com",
		CreatedAt: now,
		UpdatedAt: now,
	}
	userID++
	users[userID] = User{
		ID:        userID,
		Name:      "佐藤花子",
		Email:     "sato@example.com",
		CreatedAt: now,
		UpdatedAt: now,
	}
	userID++
}

// ==================== ユーザー関連ハンドラー ====================

// getAllUsers 全ユーザーを取得
func getAllUsers(c echo.Context) error {
	userList := []User{}
	for _, user := range users {
		userList = append(userList, user)
	}
	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "ユーザー一覧を取得しました",
		Data:    userList,
	})
}

// getUserById 指定IDのユーザーを取得
func getUserById(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なユーザーIDです",
		})
	}

	user, exists := users[id]
	if !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "ユーザーが見つかりません",
		})
	}

	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "ユーザーを取得しました",
		Data:    user,
	})
}

// createUser 新規ユーザーを作成
func createUser(c echo.Context) error {
	u := new(User)

	// リクエストボディをバインド
	if err := c.Bind(u); err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なリクエストデータです",
		})
	}

	// バリデーション
	if u.Name == "" || u.Email == "" {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "名前とメールアドレスは必須です",
		})
	}

	// 現在時刻を設定
	now := time.Now()
	u.ID = userID
	u.CreatedAt = now
	u.UpdatedAt = now

	// ユーザーを保存
	users[userID] = *u
	userID++

	return c.JSON(http.StatusCreated, CustomResponse{
		Status:  http.StatusCreated,
		Message: "ユーザーを作成しました",
		Data:    u,
	})
}

// updateUser ユーザーを更新
func updateUser(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なユーザーIDです",
		})
	}

	// ユーザーの存在確認
	if _, exists := users[id]; !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "ユーザーが見つかりません",
		})
	}

	u := new(User)
	if err := c.Bind(u); err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なリクエストデータです",
		})
	}

	// バリデーション
	if u.Name == "" || u.Email == "" {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "名前とメールアドレスは必須です",
		})
	}

	// 現在のユーザー情報を取得
	existingUser := users[id]

	// 更新
	u.ID = id
	u.CreatedAt = existingUser.CreatedAt
	u.UpdatedAt = time.Now()

	// 保存
	users[id] = *u

	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "ユーザーを更新しました",
		Data:    u,
	})
}

// partialUpdateUser ユーザーを部分的に更新(PATCH)
func partialUpdateUser(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なユーザーIDです",
		})
	}

	// ユーザーの存在確認と取得
	existingUser, exists := users[id]
	if !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "ユーザーが見つかりません",
		})
	}

	// 更新用の一時構造体
	u := new(struct {
		Name  *string `json:"name" form:"name"`
		Email *string `json:"email" form:"email"`
	})

	if err := c.Bind(u); err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なリクエストデータです",
		})
	}

	// 存在するフィールドのみ更新
	if u.Name != nil {
		existingUser.Name = *u.Name
	}
	if u.Email != nil {
		existingUser.Email = *u.Email
	}

	// タイムスタンプ更新
	existingUser.UpdatedAt = time.Now()

	// 保存
	users[id] = existingUser

	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "ユーザーを部分更新しました",
		Data:    existingUser,
	})
}

// deleteUser ユーザーを削除
func deleteUser(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なユーザーIDです",
		})
	}

	// ユーザーの存在確認
	if _, exists := users[id]; !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "ユーザーが見つかりません",
		})
	}

	// 削除
	delete(users, id)

	return c.NoContent(http.StatusNoContent)
}

// searchUsers ユーザーを検索
func searchUsers(c echo.Context) error {
	query := c.QueryParam("q")
	limit, _ := strconv.Atoi(c.QueryParam("limit"))
	offset, _ := strconv.Atoi(c.QueryParam("offset"))

	// デフォルト値の設定
	if limit <= 0 {
		limit = 10
	}
	if offset < 0 {
		offset = 0
	}

	// 検索結果
	results := []User{}
	count := 0

	// 検索ロジック
	for _, user := range users {
		// 名前またはメールアドレスに検索クエリが含まれているかチェック
		if query == "" ||
			(len(query) > 0 && (containsIgnoreCase(user.Name, query) ||
				containsIgnoreCase(user.Email, query))) {

			// オフセットをスキップ
			if count >= offset {
				results = append(results, user)
			}
			count++

			// リミットに達したら終了
			if len(results) >= limit {
				break
			}
		}
	}

	// レスポンス
	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: fmt.Sprintf("%d件のユーザーが見つかりました", len(results)),
		Data:    results,
	})
}

// ==================== 製品関連ハンドラー ====================

// getAllProducts 全製品を取得
func getAllProducts(c echo.Context) error {
	productList := []Product{}
	for _, product := range products {
		productList = append(productList, product)
	}
	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "製品一覧を取得しました",
		Data:    productList,
	})
}

// getProductById 指定IDの製品を取得
func getProductById(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効な製品IDです",
		})
	}

	product, exists := products[id]
	if !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "製品が見つかりません",
		})
	}

	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "製品を取得しました",
		Data:    product,
	})
}

// createProduct 新規製品を作成
func createProduct(c echo.Context) error {
	p := new(Product)

	// リクエストボディをバインド
	if err := c.Bind(p); err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なリクエストデータです",
		})
	}

	// バリデーション
	if p.Name == "" || p.Price <= 0 {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "名前と価格(0より大きい)は必須です",
		})
	}

	// カテゴリIDの確認
	if _, exists := categories[p.CategoryID]; !exists {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "指定されたカテゴリが存在しません",
		})
	}

	// 製品IDを設定
	p.ID = productID

	// 製品を保存
	products[productID] = *p
	productID++

	return c.JSON(http.StatusCreated, CustomResponse{
		Status:  http.StatusCreated,
		Message: "製品を作成しました",
		Data:    p,
	})
}

// updateProduct 製品を更新
func updateProduct(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効な製品IDです",
		})
	}

	// 製品の存在確認
	if _, exists := products[id]; !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "製品が見つかりません",
		})
	}

	p := new(Product)
	if err := c.Bind(p); err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なリクエストデータです",
		})
	}

	// バリデーション
	if p.Name == "" || p.Price <= 0 {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "名前と価格(0より大きい)は必須です",
		})
	}

	// カテゴリIDの確認
	if _, exists := categories[p.CategoryID]; !exists {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "指定されたカテゴリが存在しません",
		})
	}

	// 更新
	p.ID = id

	// 保存
	products[id] = *p

	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "製品を更新しました",
		Data:    p,
	})
}

// deleteProduct 製品を削除
func deleteProduct(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効な製品IDです",
		})
	}

	// 製品の存在確認
	if _, exists := products[id]; !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "製品が見つかりません",
		})
	}

	// 削除
	delete(products, id)

	return c.NoContent(http.StatusNoContent)
}

// searchProducts 製品を検索
func searchProducts(c echo.Context) error {
	// 検索パラメータをバインド
	params := new(SearchParams)
	if err := c.Bind(params); err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なリクエストパラメータです",
		})
	}

	// デフォルト値の設定
	if params.Limit <= 0 {
		params.Limit = 10
	}
	if params.Offset < 0 {
		params.Offset = 0
	}

	// 検索結果
	results := []Product{}
	count := 0

	// 検索ロジック
	for _, product := range products {
		// 名前または説明に検索クエリが含まれているかチェック
		if params.Query == "" ||
			(len(params.Query) > 0 && (containsIgnoreCase(product.Name, params.Query) ||
				containsIgnoreCase(product.Description, params.Query))) {

			// オフセットをスキップ
			if count >= params.Offset {
				results = append(results, product)
			}
			count++

			// リミットに達したら終了
			if len(results) >= params.Limit {
				break
			}
		}
	}

	// レスポンス
	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: fmt.Sprintf("%d件の製品が見つかりました", len(results)),
		Data:    results,
	})
}

// getProductsByCategory カテゴリー別に製品を取得
func getProductsByCategory(c echo.Context) error {
	categoryID, err := strconv.Atoi(c.Param("category_id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なカテゴリIDです",
		})
	}

	// カテゴリの存在確認
	category, exists := categories[categoryID]
	if !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "カテゴリが見つかりません",
		})
	}

	// カテゴリに属する製品を検索
	productList := []Product{}
	for _, product := range products {
		if product.CategoryID == categoryID {
			productList = append(productList, product)
		}
	}

	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: fmt.Sprintf("カテゴリ「%s」の製品一覧を取得しました", category.Name),
		Data:    productList,
	})
}

// ==================== カテゴリ関連ハンドラー ====================

// getAllCategories 全カテゴリを取得
func getAllCategories(c echo.Context) error {
	categoryList := []Category{}
	for _, category := range categories {
		categoryList = append(categoryList, category)
	}
	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "カテゴリ一覧を取得しました",
		Data:    categoryList,
	})
}

// getCategoryById 指定IDのカテゴリを取得
func getCategoryById(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なカテゴリIDです",
		})
	}

	category, exists := categories[id]
	if !exists {
		return c.JSON(http.StatusNotFound, CustomResponse{
			Status:  http.StatusNotFound,
			Message: "カテゴリが見つかりません",
		})
	}

	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "カテゴリを取得しました",
		Data:    category,
	})
}

// createCategory 新規カテゴリを作成
func createCategory(c echo.Context) error {
	cat := new(Category)

	// フォームデータをバインド
	if err := c.Bind(cat); err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "無効なリクエストデータです",
		})
	}

	// バリデーション
	if cat.Name == "" {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "カテゴリ名は必須です",
		})
	}

	// カテゴリIDを設定
	cat.ID = categoryID

	// カテゴリを保存
	categories[categoryID] = *cat
	categoryID++

	return c.JSON(http.StatusCreated, CustomResponse{
		Status:  http.StatusCreated,
		Message: "カテゴリを作成しました",
		Data:    cat,
	})
}

// ==================== ファイル関連ハンドラー ====================

// uploadFile ファイルをアップロード
func uploadFile(c echo.Context) error {
	// フォームからファイルを取得
	file, err := c.FormFile("file")
	if err != nil {
		return c.JSON(http.StatusBadRequest, CustomResponse{
			Status:  http.StatusBadRequest,
			Message: "ファイルが見つかりません",
		})
	}

	// ファイル名と保存パス
	filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename)
	dst := fmt.Sprintf("%s/%s", uploadDir, filename)

	// ファイルを開く
	src, err := file.Open()
	if err != nil {
		return c.JSON(http.StatusInternalServerError, CustomResponse{
			Status:  http.StatusInternalServerError,
			Message: "ファイルを開けませんでした",
		})
	}
	defer src.Close()

	// 保存先ファイルを作成
	dst_file, err := os.Create(dst)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, CustomResponse{
			Status:  http.StatusInternalServerError,
			Message: "ファイルを作成できませんでした",
		})
	}
	defer dst_file.Close()

	// ファイルをコピー
	if _, err = io.Copy(dst_file, src); err != nil {
		return c.JSON(http.StatusInternalServerError, CustomResponse{
			Status:  http.StatusInternalServerError,
			Message: "ファイルを保存できませんでした",
		})
	}

	return c.JSON(http.StatusOK, CustomResponse{
		Status:  http.StatusOK,
		Message: "ファイルをアップロードしました",
		Data: map[string]string{
			"filename": filename,
			"url":      fmt.Sprintf("/api/v1/files/%s", filename),
		},
	})
}

// getFile アップロードされたファイルを取得
func getFile(c echo.Context) error {
	filename := c.Param("filename")
	return c.File(fmt.Sprintf("%s/%s", uploadDir, filename))
}

// ==================== その他のハンドラー ====================

// pingHandler サーバーの稼働確認
func pingHandler(c echo.Context) error {
	return c.String(http.StatusOK, "pong")
}

// getServerTime サーバー時間を取得
func getServerTime(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]interface{}{
		"server_time": time.Now().Format(time.RFC3339),
		"timestamp":   time.Now().Unix(),
	})
}

// getRequestHeaders リクエストヘッダーを取得
func getRequestHeaders(c echo.Context) error {
	headers := map[string]string{}
	for k, v := range c.Request().Header {
		if len(v) > 0 {
			headers[k] = v[0]
		}
	}
	return c.JSON(http.StatusOK, headers)
}

// contextExample コンテキストの使用例
func contextExample(c echo.Context) error {
	// カスタムコンテキストの取得
	cc, ok := c.(*CustomContext)
	if !ok {
		return c.JSON(http.StatusInternalServerError, CustomResponse{
			Status:  http.StatusInternalServerError,
			Message: "カスタムコンテキストを取得できませんでした",
		})
	}

	// パスパラメータ
	c.Set("path_param_example", c.Param("id"))

	// クエリパラメータ
	c.Set("query_param_example", c.QueryParam("q"))

	// フォームパラメータ
	c.Set("form_param_example", c.FormValue("name"))

	// リクエストID(ヘッダーから)
	c.Set("request_id", c.Request().Header.Get("X-Request-ID"))

	// 開始時間(カスタムコンテキストから)
	c.Set("start_time", cc.StartTime.Format(time.RFC3339))

	return c.JSON(http.StatusOK, map[string]interface{}{
		"context_values": c.Get("path_param_example"),
		"request_path":   c.Path(),
		"start_time":     cc.StartTime,
	})
}

// streamResponse ストリーミングレスポンスの例
func streamResponse(c echo.Context) error {
	c.Response().Header().Set("Content-Type", "text/event-stream")
	c.Response().Header().Set("Cache-Control", "no-cache")
	c.Response().Header().Set("Connection", "keep-alive")
	c.Response().WriteHeader(http.StatusOK)

	// フラッシュをする
	c.Response().Flush()

	// 10回データを送信
	for i := 1; i <= 10; i++ {
		// データを書き込む
		fmt.Fprintf(c.Response(), "data: Message %d\n\n", i)
		c.Response().Flush()

		// 1秒待機
		time.Sleep(1 * time.Second)
	}

	return nil
}

// ==================== ユーティリティ関数 ====================

// containsIgnoreCase 大文字小文字を無視して文字列が含まれるかチェック
func containsIgnoreCase(s, substr string) bool {
	s = strings.ToLower(s)
	substr = strings.ToLower(substr)
	return strings.Contains(s, substr)
}

スポンサードサーチ

go run maingoの結果

go run main.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.13.3
High performance, minimalist Go web framework

____________________________________O/_______ O\ ⇨ http server started on [::]:8080

ローカルホストへのアクセス

「http://localhost:8080/」へアクセスすると、「Welcome to Echo API Learning Server」が返ってきます。

http://localhost:8080

ルーティング

ルーティングとはリクエストのURL に対応する処理(ハンドラー関数)を定義すること です。
http://localhost:8080」以外にも、
それぞれHTTPメソッドの種類から、curlリクエスト例まで書いてますので参考にしてみてください。

砕いていうと、それぞれのエンドポイントアクセスすると、それぞれの結果が返ってくるから確認して学んでねって意味です。

HTTPメソッドエンドポイント用途Echo のルート定義curlリクエスト例
GEThttp://localhost:8080/api/v1/usersすべてのユーザーを取得users.GET(“", getAllUsers)curl http://localhost:8080/api/v1/users
GEThttp://localhost:8080/api/v1/users/:id特定のユーザーを取得users.GET(“/:id", getUserById)curl http://localhost:8080/api/v1/users/1
GEThttp://localhost:8080/api/v1/users/searchユーザーを検索users.GET(“/search", searchUsers)curl “http://localhost:8080/api/v1/users/search?q=田中&limit=10&offset=0"
POSThttp://localhost:8080/api/v1/users新規ユーザーを作成users.POST(“", createUser)curl -X POST -H “Content-Type: application/json" -d “{\"name\":\"新規ユーザー\",\"email\":\"new@example.com\"}" http://localhost:8080/api/v1/users
PUThttp://localhost:8080/api/v1/users/:idユーザーを更新users.PUT(“/:id", updateUser)curl -X PUT -H “Content-Type: application/json" -d '{“name":"更新ユーザー","email":"update@example.com"}’ http://localhost:8080/api/v1/users/1
PATCHhttp://localhost:8080/api/v1/users/:idユーザーを部分的に更新users.PATCH(“/:id", partialUpdateUser)curl -X PATCH -H “Content-Type: application/json" -d '{“name":"部分更新"}’ http://localhost:8080/api/v1/users/1
DELETEhttp://localhost:8080/api/v1/users/:idユーザーを削除users.DELETE(“/:id", deleteUser)curl -X DELETE http://localhost:8080/api/v1/users/1

製品関連エンドポイント

HTTPメソッドエンドポイント用途Echo のルート定義curl リクエスト例
GEThttp://localhost:8080/api/v1/productsすべての製品を取得products.GET(“", getAllProducts)curl http://localhost:8080/api/v1/products
GEThttp://localhost:8080/api/v1/products/:id特定の製品を取得products.GET(“/:id", getProductById)curl http://localhost:8080/api/v1/products/1
POSThttp://localhost:8080/api/v1/products新規製品を作成products.POST(“", createProduct)curl -X POST -H “Content-Type: application/json" -d '{“name":"新製品","description":"説明","price":10000,"category_id":1}’ http://localhost:8080/api/v1/products
PUThttp://localhost:8080/api/v1/products/:id製品を更新products.PUT(“/:id", updateProduct)curl -X PUT -H “Content-Type: application/json" -d '{“name":"更新製品","description":"更新説明","price":15000,"category_id":1}’ http://localhost:8080/api/v1/products/1
DELETEhttp://localhost:8080/api/v1/products/:id製品を削除products.DELETE(“/:id", deleteProduct)curl -X DELETE http://localhost:8080/api/v1/products/1
GEThttp://localhost:8080/api/v1/products/search製品を検索products.GET(“/search", searchProducts)curl http://localhost:8080/api/v1/products/search?q=スマート&limit=10&offset=0
GEThttp://localhost:8080/api/v1/products/category/:category_idカテゴリ別に製品を取得products.GET(“/category/:category_id", getProductsByCategory)curl http://localhost:8080/api/v1/products/category/1

カテゴリ関連エンドポイント

HTTPメソッドエンドポイント用途Echo のルート定義curl リクエスト例
GEThttp://localhost:8080/api/v1/categoriesすべてのカテゴリを取得categories.GET(“", getAllCategories)curl http://localhost:8080/api/v1/categories
GEThttp://localhost:8080/api/v1/categories/:id特定のカテゴリを取得categories.GET(“/:id", getCategoryById)curl http://localhost:8080/api/v1/categories/1
POSThttp://localhost:8080/api/v1/categories新規カテゴリを作成categories.POST(“", createCategory)curl -X POST -H “Content-Type: application/json" -d '{“name":"新カテゴリ"}’ http://localhost:8080/api/v1/categories

ファイル関連エンドポイント

HTTPメソッドエンドポイント用途Echo のルート定義curl リクエスト例
POSThttp://localhost:8080/api/v1/uploadファイルをアップロードv1.POST(“/upload", uploadFile)curl -X POST -F “file=@/path/to/file.jpg" http://localhost:8080/api/v1/upload
GEThttp://localhost:8080/api/v1/files/:filenameアップロードされたファイルを取得v1.GET(“/files/:filename", getFile)curl http://localhost:8080/api/v1/files/filename.jpg

その他のエンドポイント

HTTPメソッドエンドポイント用途Echo のルート定義curl リクエスト例
GEThttp://localhost:8080/api/v1/pingサーバーの稼働確認v1.GET(“/ping", pingHandler)curl http://localhost:8080/api/v1/ping
GEThttp://localhost:8080/api/v1/timeサーバー時間を取得v1.GET("/time", getServerTime)curl http://localhost:8080/api/v1/time
GEThttp://localhost:8080/api/v1/headersリクエストヘッダーを取得v1.GET("/headers", getRequestHeaders)curl http://localhost:8080/api/v1/headers
GEThttp://localhost:8080/api/v1/contextコンテキストの使用例v1.GET("/context", contextExample)curl http://localhost:8080/api/v1/context
GEThttp://localhost:8080/api/v1/streamストリーミングレスポンスの例v1.GET("/stream", streamResponse)curl http://localhost:8080/api/v1/stream

HTTPメソッド

  • GET: リソースを取得するためのメソッド。データの読み取りのみを行い、サーバーの状態を変更しない
  • POST: 新しいリソースを作成するためのメソッド。フォーム送信やリソース作成に使用。
  • PUT: 既存のリソースを完全に置き換えるためのメソッド。リソース全体を更新する場合に使用。
  • PATCH: リソースを部分的に更新するためのメソッド。特定のフィールドのみを更新する場合に使用。
  • DELETE: リソースを削除するためのメソッド。指定したリソースをサーバーから削除する。

エンドポイント

  • エンドポイントはAPIの特定の機能にアクセスするためのURLパス
  • 一般的に /api/v1/リソース名 のような形式で設計される
  • パスパラメータは :id のような形式で表現される(例: /users/:id)
  • クエリパラメータは ?key=value の形式で指定される(例: /users/search?q=田中)

Echo のルート定義

  • Echoフレームワークでは e.GET(), e.POST() などでルートを定義
  • 第一引数はパスパターン、第二引数はハンドラ関数
  • グループ化して共通のプレフィックスを持つルートをまとめることができる(例: v1 := e.Group(“/api/v1"))
  • ミドルウェアを特定のルートやグループに適用可能
  • パスパラメータは :param の形式で指定される

curlリクエスト例

  • curlはコマンドラインからHTTPリクエストを送信するためのツール
  • -X オプションでHTTPメソッドを指定(例: curl -X POST)
  • -H オプションでヘッダーを設定(例: -H “Content-Type: application/json")
  • -d オプションでリクエストボディを指定(例: -d '{“name":"新規ユーザー"}’)
  • -F オプションでフォームデータとファイルを送信(例: -F “file=@/path/to/file.jpg")

スポンサードサーチ

リクエストからレスポンスまでの流れ

man.goのリクエストからレスポンスまでの処理の流れは、
以下の順番になります。

  1. クライアントからリクエスト受信
  2. ミドルウェア処理(入口側)
    • Logger ミドルウェア
    • Recover ミドルウェア
    • CORS ミドルウェア
    • CustomContext ミドルウェア
    • ResponseTime ミドルウェア
  3. ルーティングとハンドラー実行
    • リクエストデータ処理(Bind、Param、QueryParam など)
    • ビジネスロジック実行
    • レスポンスデータ生成
  4. ミドルウェア処理(出口側、逆順)
    • ResponseTime ミドルウェア(レスポンスタイム計測完了)
    • CustomContext ミドルウェア
    • CORS ミドルウェア
    • Recover ミドルウェア
    • Logger ミドルウェア
  5. クライアントへレスポンス送信

シンプルに言うと:

  1. リクエスト受信
  2. 入口側ミドルウェア
  3. ハンドラー処理
  4. 出口側ミドルウェア(逆順)
  5. レスポンス送信

リクエスト受信とミドルウェア処理

// メイン関数内のミドルウェア設定部分
e := echo.New()
e.HTTPErrorHandler = customHTTPErrorHandler

// ミドルウェアの設定
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.Use(customContextMiddleware)
e.Use(responseTimeMiddleware)

ルーティングの設定(プログラム起動時)

// メイン関数内のルーティング設定部分
// ルートグループ
e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Welcome to Echo API Learning Server")
})

// API バージョン
v1 := e.Group("/api/v1")

// ユーザー関連エンドポイント
users := v1.Group("/users")
users.GET("", getAllUsers)
users.GET("/:id", getUserById)
users.POST("", createUser)
users.PUT("/:id", updateUser)
users.PATCH("/:id", partialUpdateUser)
users.DELETE("/:id", deleteUser)
users.GET("/search", searchUsers)

リクエスト処理(クライアントからリクエスト受信時)

// レスポンスタイム計測ミドルウェア(リクエスト側)
func responseTimeMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        if cc, ok := c.(*CustomContext); ok {
            // リクエスト処理前の部分
            // cc.StartTimeが記録されている
            
            err := next(c) // ここから先のミドルウェアやハンドラーへ処理が移る
            
            // ...レスポンス側の処理(後述)...
            return err
        }
        return next(c)
    }
}

次に、ルーティングに基づいて選択されたハンドラーでリクエストが処理されます。

// createUser 新規ユーザーを作成(リクエスト処理の例)
func createUser(c echo.Context) error {
    u := new(User)
    
    // リクエストボディをバインド
    if err := c.Bind(u); err != nil {
        return c.JSON(http.StatusBadRequest, CustomResponse{
            Status:  http.StatusBadRequest,
            Message: "無効なリクエストデータです",
        })
    }
    
    // バリデーション
    if u.Name == "" || u.Email == "" {
        return c.JSON(http.StatusBadRequest, CustomResponse{
            Status:  http.StatusBadRequest,
            Message: "名前とメールアドレスは必須です",
        })
    }
    
    // 現在時刻を設定
    now := time.Now()
    u.ID = userID
    u.CreatedAt = now
    u.UpdatedAt = now
    
    // ユーザーを保存
    users[userID] = *u
    userID++
    
    // レスポンスを返す(次項へ続く)
    // ...
}

レスポンス処理(ハンドラーの処理完了後)

ハンドラー内でレスポンスが生成されます

// createUser の続き(レスポンス生成部分)
    // ...
    
    // レスポンスを返す
    return c.JSON(http.StatusCreated, CustomResponse{
        Status:  http.StatusCreated,
        Message: "ユーザーを作成しました",
        Data:    u,
    })
}

最後に、ミドルウェアのレスポンス側処理が逆順で実行されます.

// レスポンスタイム計測ミドルウェア(レスポンス側)
func responseTimeMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        if cc, ok := c.(*CustomContext); ok {
            // ...リクエスト側処理(前述)...
            
            err := next(c) // ハンドラーからの戻り
            
            // レスポンス処理(ハンドラー完了後)
            responseTime := time.Since(cc.StartTime)
            c.Response().Header().Set("X-Response-Time", responseTime.String())
            return err
        }
        return next(c)
    }
}

このような、一連の流れとなっています。

  1. ミドルウェア設定
  2. ルーティング設定
  3. リクエスト受信時の処理(ミドルウェア入口側 → ハンドラー)
  4. レスポンス生成と送信(ハンドラー → ミドルウェア出口側)

EchoのContextとは?

リクエストやレスポンス前に、EchoのContextを覚えておきましょう

Context は、GoのWebフレームワーク Echo における リクエストとレスポンスの情報を管理するオブジェクト です。echo.Context 型のオブジェクトとして提供され、各リクエストごとに生成されます。

メソッド役割
Context#Bindリクエストボディのデータを指定した構造体にバインドする (c.Bind(&struct))
Context#Paramルートパラメータ(URLの /:id など)を取得する (c.Param(“id"))
Context#QueryParamクエリパラメータ(?key=value 形式の値)を取得する (c.QueryParam(“key"))
Context#JSONJSONレスポンスを返す (c.JSON(http.StatusOK, data))
Context#NoContentレスポンスボディなしで、ステータスコードを返す (c.NoContent(http.StatusNoContent))
Context#Stringテキストレスポンスを返す (c.String(http.StatusOK, “message"))
Context#Fileファイルレスポンスを返す
(c.File(“path/file"))
icon

Goのechoの学習は以上になります。

GoGo

Posted by kami