今天要補兩個超實用的動作:
完成今天,你就能在清單上打勾✅、改字✍️,資料開始會呼吸。
本篇你會做什麼
• 寫一個 PUT /todos/:id
(可同時改 title
與 done
)
• 寫一個 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}
則是明確設定。
小結 & 下一步
恭喜!現在你已經會 新增、查詢、更新。清單已經七分成熟🍲。