第43课 - REST API 设计与实现
学习目标
完成本课学习后,你将能够:
- 理解RESTful架构风格的核心原则与HTTP方法的语义。
- 使用Go标准库
net/http构建一个完整的、支持CRUD操作的RESTful API。 - 正确处理JSON格式的请求体与响应体。
- 使用路由(Router)组织和管理API端点。
- 了解在并发环境下处理HTTP请求的基本模式。
核心概念
REST (Representational State Transfer) 不是一个协议,而是一种设计风格。想象一个餐厅:你(客户端)拿着菜单(API文档),告诉服务员(服务器)你想要什么菜(资源),用特定的方式(HTTP方法)。服务员给你做好的菜(资源状态)。
- 资源 (Resource):一切皆资源,比如一个用户、一篇文章、一本书。它通过URI(统一资源标识符)来标识,例如
/api/books/123。 - HTTP 方法 (动词):定义了你对资源要执行的动作。
GET:读取资源。无副作用,幂等。POST:创建新资源。通常向集合端点/api/books提交数据。PUT:替换/更新整个资源。向资源端点/api/books/123发送完整数据。DELETE:删除资源。向资源端点/api/books/123发起请求。PATCH:部分更新资源(本课示例中暂不实现)。
- 状态码 (Status Codes):服务器的回复暗号。
200 OK:通用成功。201 Created:资源创建成功(通常与POST配合使用)。204 No Content:操作成功,但没有内容返回(常用于DELETE)。400 Bad Request:客户端请求有误(如JSON格式错误)。404 Not Found:资源不存在。500 Internal Server Error:服务器内部错误。
- JSON:前后端数据交换的“通用语言”。Go通过
encoding/json包方便地进行序列化(Go结构体 → JSON)与反序列化(JSON → Go结构体)。
路由 (Routing) 就像餐厅的不同柜台。甜点柜台(/desserts)和主食柜台(/main-courses)处理不同的请求。在Go中,http.ServeMux 就是一个简单的路由器。
代码示例:一个简单的书籍管理API
我们将构建一个管理书籍的RESTful API,支持增删改查。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
)
// Book 定义了书籍的资源结构
type Book struct {
ID int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
}
// 模拟数据库:一个全局的map和一把并发锁
var (
books = make(map[int]Book)
nextID = 1
booksMu sync.RWMutex // 保护books的并发读写锁
)
func main() {
// 初始化几条数据
books[nextID] = Book{ID: nextID, Title: "The Go Programming Language", Author: "Alan A. A. Donovan"}
nextID++
books[nextID] = Book{ID: nextID, Title: "Clean Code", Author: "Robert C. Martin"}
nextID++
// 路由设置
mux := http.NewServeMux()
mux.HandleFunc("/api/books", booksHandler) // 处理 /api/books 和 /api/books/
mux.HandleFunc("/api/books/", bookHandler) // 处理 /api/books/{id}
fmt.Println("Server starting on :8080...")
log.Fatal(http.ListenAndServe(":8080", mux))
}
// booksHandler 处理对书籍集合的请求 (GET, POST)
func booksHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// 获取所有书籍列表
booksMu.RLock()
bookList := make([]Book, 0, len(books))
for _, b := range books {
bookList = append(bookList, b)
}
booksMu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(bookList)
case http.MethodPost:
// 创建一本新书
var newBook Book
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&newBook); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
// 注意:客户端通常不指定ID,我们从服务端分配
booksMu.Lock()
newBook.ID = nextID
books[newBook.ID] = newBook
nextID++
booksMu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) // 201 Created
json.NewEncoder(w).Encode(newBook)
default:
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
}
}
// bookHandler 处理对单个书籍的请求 (GET, PUT, DELETE)
func bookHandler(w http.ResponseWriter, r *http.Request) {
// 从路径中解析书籍ID,例如 /api/books/1 -> id=1
idStr := strings.TrimPrefix(r.URL.Path, "/api/books/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, `{"error": "Invalid book ID"}`, http.StatusBadRequest)
return
}
// 根据HTTP方法处理
switch r.Method {
case http.MethodGet:
booksMu.RLock()
book, exists := books[id]
booksMu.RUnlock()
if !exists {
http.Error(w, `{"error": "Book not found"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(book)
case http.MethodPut:
// 更新一本书
booksMu.Lock()
_, exists := books[id]
if !exists {
booksMu.Unlock()
http.Error(w, `{"error": "Book not found"}`, http.StatusNotFound)
return
}
var updatedBook Book
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&updatedBook); err != nil {
booksMu.Unlock()
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
// 确保路径ID与请求体中的ID一致(或忽略请求体中的ID)
updatedBook.ID = id
books[id] = updatedBook
booksMu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedBook)
case http.MethodDelete:
// 删除一本书
booksMu.Lock()
_, exists := books[id]
if !exists {
booksMu.Unlock()
http.Error(w, `{"error": "Book not found"}`, http.StatusNotFound)
return
}
delete(books, id)
booksMu.Unlock()
w.WriteHeader(http.StatusNoContent) // 204 No Content
default:
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
}
}
如何测试这个API?
- 运行程序:
go run main.go - 使用
curl命令(或Postman等工具)进行测试:- 获取所有书籍:
curl http://localhost:8080/api/books - 创建一本新书:
curl -X POST -H "Content-Type: application/json" -d '{"title": "Effective Go", "author": "Go Team"}' http://localhost:8080/api/books - 获取单本书:
curl http://localhost:8080/api/books/1 - 更新一本书:
curl -X PUT -H "Content-Type: application/json" -d '{"title": "Effective Go (Updated)", "author": "Go Team"}' http://localhost:8080/api/books/1 - 删除一本书:
curl -X DELETE http://localhost:8080/api/books/2
- 获取所有书籍:
实践练习
-
简单扩展(易):为
/api/books端点添加搜索功能。当请求带有查询参数?author=xxx时,只返回作者名包含该关键字的书籍。- 要求:在
booksHandler的GET逻辑中解析author参数。 - 预期输出:
curl "http://localhost:8080/api/books?author=Martin"应返回作者为“Robert C. Martin”的书籍。
- 要求:在
-
嵌套路由(中):为用户添加书籍收藏功能。设计新的API端点:
GET /api/users/{userId}/books:获取某个用户收藏的书籍ID列表。POST /api/users/{userId}/books/{bookId}:收藏一本书。- 提示:需要新增
User结构体和对应的存储。思考如何组织路由和处理函数。
-
并发控制(难):修改当前代码,移除全局的
booksMu互斥锁。你的任务是:仅在nextID自增时使用锁保护,而books这个map的读写完全不加锁。运行程序,并用curl快速并发地创建多本书(可使用ab或wrk工具),观察程序是否出现fatal error: concurrent map read and map write异常。分析原因,并思考为什么必须使用锁(或分片、sync.Map等)来保护共享数据结构。
常见错误
- 忘记设置响应头:返回JSON数据时,忘记设置
Content-Type: application/json,导致客户端(尤其是浏览器)可能无法正确解析。 - 路由冲突与优先级:
/api/books和/api/books/在Go的ServeMux中是同一个路由。但如果定义了/api/books和/api/books/search,需要注意匹配的精确性。更复杂的路由可能需要第三方库(如gorilla/mux)。 - 请求体未读取或重复读取:对于POST/PUT请求,
r.Body只能被读取一次。如果在中间件和主处理器中都需要读取,需要预先存储读取的内容。 - JSON标签拼写错误:结构体标签
json:"title"中的引号或字段名写错,会导致JSON编码/解码失败或字段名不符预期。 - 并发数据竞争:如示例所示,对共享的全局变量(如
booksmap)进行读写操作时,如果不加锁,在高并发下会导致程序崩溃。这是Go并发编程中最常见的问题之一。
小结
- REST的核心是资源和对资源的操作,这些操作由HTTP方法(GET, POST, PUT, DELETE)明确定义。
- Go标准库足以构建生产级API。
net/http提供了强大的基础,encoding/json处理了数据序列化。 - 状态码是API通信的重要部分,正确使用它们能让客户端更准确地理解服务器响应。
- 路由是组织API的骨架,清晰的路由设计让API易于理解和使用。
- 并发安全至关重要。在Web服务器这个并发环境下,必须使用锁(
sync.Mutex、sync.RWMutex)或其他同步机制来保护共享状态,否则程序会随机崩溃。 - 更复杂的路由匹配(如路径参数
/api/books/{id}、方法过滤)可以考虑使用第三方库如gorilla/mux或chi,它们提供了更强大和便捷的功能。
练习编辑器
go
Loading...