【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")) |
C ontext#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の学習は以上になります。