iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Modern Web

Golang x Echo 30 天:零基礎GO , 後端入門系列 第 14

用 Go + Echo 打造你的第一個 TodoList , 第 4 篇:更新任務 API — PUT 與 PATCH

  • 分享至 

  • xImage
  •  

今天要補兩個超實用的動作:

  • PUT /todos/:id:更新整筆任務(例如改標題、設定完成狀態)
  • PATCH /todos/:id/done:只更新「完成狀態」這個欄位(最常見的勾選動作)

完成今天,你就能在清單上打勾✅、改字✍️,資料開始會呼吸


本篇你會做什麼

• 寫一個 PUT /todos/:id(可同時改 titledone

• 寫一個 PATCH /todos/:id/done(只改 done,可用 true/false 或「切換」兩種模式)

• 練習用 curl 測試

• 補上小工具函式:findIndexByID,程式更好讀


先回顧:資料結構 & 暫存

type Todo struct { 
    ID    int    `json:"id"` 
    Title string `json:"title"` 
    Done  bool   `json:"done"` 
} 
 
var todos []Todo 
var nextID = 1 

工具函式:用 ID 找索引

func findIndexByID(id int) int { 
    for i, t := range todos { 
        if t.ID == id { 
            return i 
        } 
    } 
    return -1 
} 

路由 1:PUT /todos/:id(整筆更新)

e.PUT("/todos/:id", func(c echo.Context) error { 
    idStr := c.Param("id") 
    id, err := strconv.Atoi(idStr) 
    if err != nil { 
        return c.JSON(http.StatusBadRequest, map[string]string{ 
            "error": "id must be a number", 
        }) 
    } 
 
    // 解析請求 JSON 
    var req struct { 
        Title string `json:"title"` 
        Done  *bool  `json:"done"` // 用指標:前端沒傳就不會誤判為 false 
    } 
    if err := c.Bind(&req); err != nil { 
        return c.JSON(http.StatusBadRequest, map[string]string{ 
            "error": "invalid json", 
        }) 
    } 
 
    // 基本驗證 
    if strings.TrimSpace(req.Title) == "" { 
        return c.JSON(http.StatusBadRequest, map[string]string{ 
            "error": "title cannot be empty", 
        }) 
    } 
 
    idx := findIndexByID(id) 
    if idx == -1 { 
        return c.JSON(http.StatusNotFound, map[string]string{ 
            "error": "todo not found", 
        }) 
    } 
 
    // 更新 
    todos[idx].Title = req.Title 
    if req.Done != nil { // 有傳才更新 
        todos[idx].Done = *req.Done 
    } 
 
    return c.JSON(http.StatusOK, todos[idx]) 
}) 

測試 PUT:

curl -X PUT http://localhost:1323/todos/1   -H "Content-Type: application/json"   -d '{"title":"寫數學作業(已交)","done":true}' 

回應(示意):

{ 
  "id": 1, 
  "title": "寫數學作業(已交)", 
  "done": true 
} 

路由 2:PATCH /todos/:id/done(只改完成狀態)

e.PATCH("/todos/:id/done", func(c echo.Context) error { 
    idStr := c.Param("id") 
    id, err := strconv.Atoi(idStr) 
    if err != nil { 
        return c.JSON(http.StatusBadRequest, map[string]string{ 
            "error": "id must be a number", 
        }) 
    } 
 
    idx := findIndexByID(id) 
    if idx == -1 { 
        return c.JSON(http.StatusNotFound, map[string]string{ 
            "error": "todo not found", 
        }) 
    } 
 
    // 嘗試解析 body;若沒有 body,就走「切換模式」 
    var req struct { 
        Done *bool `json:"done"` 
    } 
    _ = c.Bind(&req) // 解析失敗也不致命,req.Done 仍為 nil 
 
    if req.Done == nil { 
        // 切換模式 
        todos[idx].Done = !todos[idx].Done 
    } else { 
        // 明確設定 
        todos[idx].Done = *req.Done 
    } 
 
    return c.JSON(http.StatusOK, todos[idx]) 
}) 

測試 PATCH:

# 明確設定:把 id=2 設為完成 
curl -X PATCH http://localhost:1323/todos/2/done   -H "Content-Type: application/json"   -d '{"done":true}' 
 
# 切換模式:不帶 body,直接反轉 id=2 的 done 
curl -X PATCH http://localhost:1323/todos/2/done 

完整 main.go(含新增/查詢/更新)

package main 
 
import ( 
	"net/http" 
	"strconv" 
	"strings" 
 
	"github.com/labstack/echo/v4" 
) 
 
type Todo struct { 
	ID    int    `json:"id"` 
	Title string `json:"title"` 
	Done  bool   `json:"done"` 
} 
 
var todos []Todo 
var nextID = 1 
 
func findIndexByID(id int) int { 
	for i, t := range todos { 
		if t.ID == id { 
			return i 
		} 
	} 
	return -1 
} 
 
func main() { 
	e := echo.New() 
 
	// Hello 
	e.GET("/hello", func(c echo.Context) error { 
		return c.String(http.StatusOK, "Hello, TodoList!") 
	}) 
 
	// 新增任務:POST /todos 
	e.POST("/todos", func(c echo.Context) error { 
		var newTodo Todo 
		if err := c.Bind(&newTodo); err != nil or strings.TrimSpace(newTodo.Title) == "" { 
			return c.JSON(http.StatusBadRequest, map[string]string{ 
				"error": "invalid request", 
			}) 
		} 
		newTodo.ID = nextID 
		newTodo.Done = false 
		nextID++ 
		todos = append(todos, newTodo) 
		return c.JSON(http.StatusOK, newTodo) 
	}) 
 
	// 列出全部:GET /todos 
	e.GET("/todos", func(c echo.Context) error { 
		return c.JSON(http.StatusOK, todos) 
	}) 
 
	// 查單一:GET /todos/:id 
	e.GET("/todos/:id", func(c echo.Context) error { 
		idStr := c.Param("id") 
		id, err := strconv.Atoi(idStr) 
		if err != nil { 
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "id must be a number"}) 
		} 
		idx := findIndexByID(id) 
		if idx == -1 { 
			return c.JSON(http.StatusNotFound, map[string]string{"error": "todo not found"}) 
		} 
		return c.JSON(http.StatusOK, todos[idx]) 
	}) 
 
	// 整筆更新:PUT /todos/:id 
	e.PUT("/todos/:id", func(c echo.Context) error { 
		idStr := c.Param("id") 
		id, err := strconv.Atoi(idStr) 
		if err != nil { 
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "id must be a number"}) 
		} 
		var req struct { 
			Title string `json:"title"` 
			Done  *bool  `json:"done"` 
		} 
		if err := c.Bind(&req); err != nil { 
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid json"}) 
		} 
		if strings.TrimSpace(req.Title) == "" { 
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "title cannot be empty"}) 
		} 
		idx := findIndexByID(id) 
		if idx == -1 { 
			return c.JSON(http.StatusNotFound, map[string]string{"error": "todo not found"}) 
		} 
		todos[idx].Title = req.Title 
		if req.Done != nil { 
			todos[idx].Done = *req.Done 
		} 
		return c.JSON(http.StatusOK, todos[idx]) 
	}) 
 
	// 部分更新(完成狀態):PATCH /todos/:id/done 
	e.PATCH("/todos/:id/done", func(c echo.Context) error { 
		idStr := c.Param("id") 
		id, err := strconv.Atoi(idStr) 
		if err != nil { 
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "id must be a number"}) 
		} 
		idx := findIndexByID(id) 
		if idx == -1 { 
			return c.JSON(http.StatusNotFound, map[string]string{"error": "todo not found"}) 
		} 
		var req struct { 
			Done *bool `json:"done"` 
		} 
		_ = c.Bind(&req) 
		if req.Done == nil { 
			// 切換模式 
			todos[idx].Done = !todos[idx].Done 
		} else { 
			todos[idx].Done = *req.Done 
		} 
		return c.JSON(http.StatusOK, todos[idx]) 
	}) 
 
	e.Logger.Fatal(e.Start(":1323")) 
} 

測試清單(照著抄就會動)

go run main.go 
curl -X POST http://localhost:1323/todos   -H "Content-Type: application/json"   -d '{"title":"寫數學作業"}' 
 
curl -X POST http://localhost:1323/todos   -H "Content-Type: application/json"   -d '{"title":"倒垃圾"}' 
curl http://localhost:1323/todos 
curl -X PUT http://localhost:1323/todos/1   -H "Content-Type: application/json"   -d '{"title":"寫數學作業(已交)","done":true}' 
curl -X PATCH http://localhost:1323/todos/2/done   -H "Content-Type: application/json"   -d '{"done":true}' 
curl -X PATCH http://localhost:1323/todos/2/done 

常見小坑(小抄放口袋)

title cannot be empty:PUT 更新時,title 不能是空白或全空格。

id must be a number:網址裡的 :id 必須是數字。

todo not found:找不到這個 id,先列出全部看看目前有哪些任務。

• PATCH 沒帶 body 是切換模式;帶了 {"done": true/false} 則是明確設定


小結 & 下一步

恭喜!現在你已經會 新增、查詢、更新。清單已經七分成熟🍲。


上一篇
用 Go + Echo 打造你的第一個 TodoList , 第 3 篇:查詢任務:GET /todos
下一篇
用 Go + Echo 打造你的第一個 TodoList , 第 5 篇:刪除任務 + JSON 檔持久化
系列文
Golang x Echo 30 天:零基礎GO , 後端入門16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言