用Go實現(xiàn)一個帶緩存的REST API服務(wù)端
1.REST API原理
REST(Representational State Transfer)是一種通過HTTP協(xié)議設(shè)計API的架構(gòu)風(fēng)格,用于構(gòu)建分布式系統(tǒng)中的網(wǎng)絡(luò)應(yīng)用程序。REST API(RESTful API)是基于這種設(shè)計風(fēng)格的應(yīng)用程序編程接口。其主要優(yōu)點是其極大的靈活性, 只要需要直接從服務(wù)器向Web應(yīng)用程序或站點的用戶提供數(shù)據(jù), 開發(fā)人員直接使用REST API即可實現(xiàn)。
REST API 的設(shè)計目的是創(chuàng)建簡單、可伸縮、可維護(hù)且具有良好可讀性的接口, 以促進(jìn)客戶端和服務(wù)器之間的有效通信, 通過使用HTTP協(xié)議和一組統(tǒng)一的設(shè)計原則, REST API在實現(xiàn)上具備一些特定的屬性:
- 資源(Resources):在REST中, 數(shù)據(jù)或服務(wù)都被視為資源。每個資源都有一個唯一的標(biāo)識符, 用于在網(wǎng)絡(luò)上標(biāo)識和定位該資源。
- 表現(xiàn)層(Representation):資源的狀態(tài)可以以不同的表現(xiàn)形式呈現(xiàn), 例如: JSON、XML或HTML。客戶端可以通過請求特定的表現(xiàn)形式來與服務(wù)器交互。
- 狀態(tài)無關(guān)(Stateless):REST是狀態(tài)無關(guān)的, 這意味著每個請求從客戶端到服務(wù)器都包含了足夠的信息, 服務(wù)器不需要存儲客戶端的狀態(tài)。每個請求都應(yīng)該包含了執(zhí)行該請求所需的所有信息。
- 統(tǒng)一接口(Uniform Interface):RESTful API的設(shè)計應(yīng)該遵循一致的接口原則, 使得不同的組件之間的通信變得簡單統(tǒng)一。
- 無狀態(tài)通信(Stateless Communication):每個請求從客戶端到服務(wù)器都應(yīng)該包含足夠的信息, 以便服務(wù)器能夠理解和處理請求,而無需依賴之前的請求。
- 緩存(Cacheability):REST API支持緩存, 以提高性能和減輕服務(wù)器的負(fù)擔(dān)。服務(wù)器可以在響應(yīng)中指定數(shù)據(jù)的緩存策略,而客戶端可以使用緩存來避免重復(fù)請求相同的數(shù)據(jù)。
- 使用標(biāo)準(zhǔn)方法(Standard Methods):REST使用標(biāo)準(zhǔn)的HTTP方法,如:GET、POST、PUT、DELETE來執(zhí)行不同的操作, 這些方法對應(yīng)于對資源的不同操作, 使得API的使用更加直觀和符合HTTP標(biāo)準(zhǔn)。
2.REST API服務(wù)端設(shè)計
下面我將使用Go語言來設(shè)計一個REST API的服務(wù)端, 這里的模擬場景是通過服務(wù)端來對外提供文章的增、刪、查服務(wù), 文章的查詢方式包括兩種: 1.查詢服務(wù)器所有文章內(nèi)容。2.根據(jù)文章ID查詢某篇文章的內(nèi)容。
文章的屬性包括三個字段: 文章ID、文章標(biāo)題、文章內(nèi)容, 這里可以用一個結(jié)構(gòu)體表示:
type Article struct {
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Content string `json:"content,omitempty"`
}
由于我們設(shè)計了四個功能接口, 將其轉(zhuǎn)換成接口代碼框架如下:
// 獲取所有文章接口
func GetArticles(w http.ResponseWriter, r *http.Request) {
...
}
// 獲取單篇文章接口
func GetArticle(w http.ResponseWriter, r *http.Request) {
...
}
// 創(chuàng)建文章接口
func CreateArticle(w http.ResponseWriter, r *http.Request) {
...
}
// 刪除文章接口
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
...
}
3.功能代碼實現(xiàn)
首先來實現(xiàn)獲取所有文章的接口, 具體參考代碼如下:
// 獲取所有文章
func GetArticles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// 嘗試從緩存中獲取文章列表
if cachedArticles, found := articleCache.Load("all"); found {
json.NewEncoder(w).Encode(cachedArticles)
return
}
// 從原始數(shù)據(jù)源獲取文章列表
json.NewEncoder(w).Encode(Articles)
// 將文章列表存入緩存
articleCache.Store("all", Articles)
}
獲取所有文章首先是嘗試從緩存中獲取文章列表, 緩存cachedArticles這里實際是一個sync.Map類型的變量, 支持并發(fā)安全, 如果找到的話, 直接序列化成JSON的格式返回。如果緩存中沒有找到, 則從原始數(shù)據(jù)源中獲取文章列表(實際應(yīng)用中應(yīng)該是從數(shù)據(jù)庫中獲取)并序列化成JSON返回。并把該文章列表存入緩存之中。
接下來實現(xiàn)第二個接口,通過文章ID來查詢, 實現(xiàn)參考代碼如下:
// 獲取單篇文章
func GetArticle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
params := mux.Vars(r)
// 嘗試從緩存中獲取單個文章
if cachedArticle, found := articleCache.Load(params["id"]); found {
json.NewEncoder(w).Encode(cachedArticle)
return
}
// 從原始數(shù)據(jù)源獲取單個文章
for _, article := range Articles {
if article.ID == params["id"] {
json.NewEncoder(w).Encode(article)
// 將單個文章存入緩存
articleCache.Store(params["id"], article)
return
}
}
json.NewEncoder(w).Encode(&Article{})
}
首先將傳入的ID參數(shù)在緩存中進(jìn)行查找, 找到則直接返回JSON數(shù)據(jù)。如果沒有找到則繼續(xù)在文章列表中查找, 并將單篇文章存入緩存。
創(chuàng)建文章接口參考代碼如下:
// 創(chuàng)建文章
func CreateArticle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var article Article
_ = json.NewDecoder(r.Body).Decode(&article)
Articles = append(Articles, article)
// 清除所有文章緩存
articleCache.Delete("all")
json.NewEncoder(w).Encode(Articles)
}
創(chuàng)建文章列表需要注意的是, 這里為了維護(hù)緩存一致性,避免臟數(shù)據(jù), 對緩存進(jìn)行了清空, 以便下次GetArticles()時更新最新的緩存。
同理也不難實現(xiàn)刪除文章的接口:
// 刪除文章
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
params := mux.Vars(r)
// 清除單個文章緩存
articleCache.Delete(params["id"])
for index, article := range Articles {
if article.ID == params["id"] {
Articles = append(Articles[:index], Articles[index+1:]...)
break
}
}
// 清除所有文章緩存
articleCache.Delete("all")
json.NewEncoder(w).Encode(Articles)
}
最后, 在main函數(shù)中, 我們需要往列表中添加一些數(shù)據(jù)來模擬服務(wù)端保存的文章數(shù), 并且定義四個接口的路由, 整體代碼如下:
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"github.com/gorilla/mux"
)
// Article 結(jié)構(gòu)體表示 API 中的數(shù)據(jù)模型
type Article struct {
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Content string `json:"content,omitempty"`
}
// Articles 數(shù)組用于存儲文章數(shù)據(jù)
var Articles []Article
var articleCache sync.Map
// 獲取所有文章
func GetArticles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// 嘗試從緩存中獲取文章列表
if cachedArticles, found := articleCache.Load("all"); found {
json.NewEncoder(w).Encode(cachedArticles)
return
}
// 從原始數(shù)據(jù)源獲取文章列表
json.NewEncoder(w).Encode(Articles)
// 將文章列表存入緩存
articleCache.Store("all", Articles)
}
// 獲取單個文章
func GetArticle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
params := mux.Vars(r)
// 嘗試從緩存中獲取單個文章
if cachedArticle, found := articleCache.Load(params["id"]); found {
json.NewEncoder(w).Encode(cachedArticle)
return
}
// 從原始數(shù)據(jù)源獲取單個文章
for _, article := range Articles {
if article.ID == params["id"] {
json.NewEncoder(w).Encode(article)
// 將單個文章存入緩存
articleCache.Store(params["id"], article)
return
}
}
json.NewEncoder(w).Encode(&Article{})
}
// 創(chuàng)建文章
func CreateArticle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var article Article
_ = json.NewDecoder(r.Body).Decode(&article)
Articles = append(Articles, article)
// 清除所有文章緩存
articleCache.Delete("all")
json.NewEncoder(w).Encode(Articles)
}
// 刪除文章
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
params := mux.Vars(r)
// 清除單個文章緩存
articleCache.Delete(params["id"])
for index, article := range Articles {
if article.ID == params["id"] {
Articles = append(Articles[:index], Articles[index+1:]...)
break
}
}
// 清除所有文章緩存
articleCache.Delete("all")
json.NewEncoder(w).Encode(Articles)
}
func main() {
// 初始化數(shù)據(jù)
Articles = append(Articles, Article{ID: "1", Title: "Article 1", Content: "Content 1"})
Articles = append(Articles, Article{ID: "2", Title: "Article 2", Content: "Content 2"})
// 創(chuàng)建路由器
router := mux.NewRouter()
// 定義路由處理程序
router.HandleFunc("/articles", GetArticles).Methods(http.MethodGet)
router.HandleFunc("/articles/{id}", GetArticle).Methods(http.MethodGet)
router.HandleFunc("/articles", CreateArticle).Methods(http.MethodPost)
router.HandleFunc("/articles/{id}", DeleteArticle).Methods(http.MethodDelete)
// 啟動服務(wù)器
log.Fatal(http.ListenAndServe(":8080", router))
}
4.實際運行效果
在本地運行服務(wù)端, 服務(wù)端將在本地監(jiān)聽8080端口, 通過瀏覽器輸入: http://127.0.0.1:8080/articles, 該接口將獲取所有文章,如圖:
通過API接口通過文章ID查詢某一篇文章,接口請求如下:
新增一條新文章是POST請求, 這里主要通過Apifox發(fā)送POST請求,如圖:
從返回的結(jié)果來看, 成功添加了一條新記錄, 再次使用獲取全部文章接口看一下,如圖:
新記錄確實添加成功。
最后, 還是通過Apifox,我們發(fā)送一個刪除ID為2的文章請求,如圖:
從Apifox返回的結(jié)果來看,確實刪除成功了, 現(xiàn)在再獲取一下全部文章,如圖:
所有接口全部驗證成功。