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

今回は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リクエスト例 | 
|---|---|---|---|---|
| GET | http://localhost:8080/api/v1/users | すべてのユーザーを取得 | users.GET(“", getAllUsers) | curl http://localhost:8080/api/v1/users | 
| GET | http://localhost:8080/api/v1/users/:id | 特定のユーザーを取得 | users.GET(“/:id", getUserById) | curl http://localhost:8080/api/v1/users/1 | 
| GET | http://localhost:8080/api/v1/users/search | ユーザーを検索 | users.GET(“/search", searchUsers) | curl “http://localhost:8080/api/v1/users/search?q=田中&limit=10&offset=0" | 
| POST | http://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 | 
| PUT | http://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 | 
| PATCH | http://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 | 
| DELETE | http://localhost:8080/api/v1/users/:id | ユーザーを削除 | users.DELETE(“/:id", deleteUser) | curl -X DELETE http://localhost:8080/api/v1/users/1 | 
製品関連エンドポイント
| HTTPメソッド | エンドポイント | 用途 | Echo のルート定義 | curlリクエスト例 | 
|---|---|---|---|---|
| GET | http://localhost:8080/api/v1/products | すべての製品を取得 | products.GET(“", getAllProducts) | curl http://localhost:8080/api/v1/products | 
| GET | http://localhost:8080/api/v1/products/:id | 特定の製品を取得 | products.GET(“/:id", getProductById) | curl http://localhost:8080/api/v1/products/1 | 
| POST | http://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 | 
| PUT | http://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 | 
| DELETE | http://localhost:8080/api/v1/products/:id | 製品を削除 | products.DELETE(“/:id", deleteProduct) | curl -X DELETE http://localhost:8080/api/v1/products/1 | 
| GET | http://localhost:8080/api/v1/products/search | 製品を検索 | products.GET(“/search", searchProducts) | curl http://localhost:8080/api/v1/products/search?q=スマート&limit=10&offset=0 | 
| GET | http://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リクエスト例 | 
|---|---|---|---|---|
| GET | http://localhost:8080/api/v1/categories | すべてのカテゴリを取得 | categories.GET(“", getAllCategories) | curl http://localhost:8080/api/v1/categories | 
| GET | http://localhost:8080/api/v1/categories/:id | 特定のカテゴリを取得 | categories.GET(“/:id", getCategoryById) | curl http://localhost:8080/api/v1/categories/1 | 
| POST | http://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リクエスト例 | 
|---|---|---|---|---|
| POST | http://localhost:8080/api/v1/upload | ファイルをアップロード | v1.POST(“/upload", uploadFile) | curl -X POST -F “file=@/path/to/file.jpg" http://localhost:8080/api/v1/upload | 
| GET | http://localhost:8080/api/v1/files/:filename | アップロードされたファイルを取得 | v1.GET(“/files/:filename", getFile) | curl http://localhost:8080/api/v1/files/filename.jpg | 
その他のエンドポイント
| HTTPメソッド | エンドポイント | 用途 | Echo のルート定義 | curlリクエスト例 | 
|---|---|---|---|---|
| GET | http://localhost:8080/api/v1/ping | サーバーの稼働確認 | v1.GET(“/ping", pingHandler) | curl http://localhost:8080/api/v1/ping | 
| GET | http://localhost:8080/api/v1/time | サーバー時間を取得 | v1.GET("/time", getServerTime) | curl http://localhost:8080/api/v1/time | 
| GET | http://localhost:8080/api/v1/headers | リクエストヘッダーを取得 | v1.GET("/headers", getRequestHeaders) | curl http://localhost:8080/api/v1/headers | 
| GET | http://localhost:8080/api/v1/context | コンテキストの使用例 | v1.GET("/context", contextExample) | curl http://localhost:8080/api/v1/context | 
| GET | http://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のリクエストからレスポンスまでの処理の流れは、
以下の順番になります。
- クライアントからリクエスト受信
- ミドルウェア処理(入口側)
- Logger ミドルウェア
- Recover ミドルウェア
- CORS ミドルウェア
- CustomContext ミドルウェア
- ResponseTime ミドルウェア
 
- ルーティングとハンドラー実行
- リクエストデータ処理(Bind、Param、QueryParam など)
- ビジネスロジック実行
- レスポンスデータ生成
 
- ミドルウェア処理(出口側、逆順)
- ResponseTime ミドルウェア(レスポンスタイム計測完了)
- CustomContext ミドルウェア
- CORS ミドルウェア
- Recover ミドルウェア
- Logger ミドルウェア
 
- クライアントへレスポンス送信
シンプルに言うと:
- リクエスト受信
- 入口側ミドルウェア
- ハンドラー処理
- 出口側ミドルウェア(逆順)
- レスポンス送信
リクエスト受信とミドルウェア処理
// メイン関数内のミドルウェア設定部分
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)
    }
}
このような、一連の流れとなっています。
- ミドルウェア設定
- ルーティング設定
- リクエスト受信時の処理(ミドルウェア入口側 → ハンドラー)
- レスポンス生成と送信(ハンドラー → ミドルウェア出口側)
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#JSON | JSONレスポンスを返す (c.JSON(http.StatusOK, data)) | 
| Context#NoContent | レスポンスボディなしで、ステータスコードを返す (c.NoContent(http.StatusNoContent)) | 
| Context#String | テキストレスポンスを返す (c.String(http.StatusOK, “message")) | 
| Context#File | ファイルレスポンスを返す (c.File(“path/file")) | 

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


 https://echo.labstack.com/echo-api
 https://echo.labstack.com/echo-api





