الگوهای طراحی API
ساخت یک API RESTful با Gin فراتر از تعریف مسیرها است. یک API خوب طراحی شده از فرمتهای پاسخ سازگار، صفحهبندی قابل پیشبینی، نسخهبندی واضح و مدیریت خطای ساختارمند استفاده میکند. این راهنما الگوهای عملی را پوشش میدهد که میتوانید در برنامههای Gin تولیدی اعمال کنید.
فرمت پاسخ سازگار
بستهبندی هر پاسخ در یک پوشش استاندارد زندگی را برای مصرفکنندگان API آسانتر میکند. آنها همیشه میدانند دادهها، خطاها و فرادادهها کجا هستند.
package main
import ( "net/http"
"github.com/gin-gonic/gin")
// Response is the standard API envelope.type Response struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error *ErrorInfo `json:"error,omitempty"` Meta *Meta `json:"meta,omitempty"`}
type ErrorInfo struct { Code string `json:"code"` Message string `json:"message"`}
type Meta struct { Page int `json:"page,omitempty"` PerPage int `json:"per_page,omitempty"` Total int `json:"total,omitempty"` TotalPages int `json:"total_pages,omitempty"`}
// OK sends a success response.func OK(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, Response{ Success: true, Data: data, })}
// Fail sends an error response.func Fail(c *gin.Context, status int, code, message string) { c.JSON(status, Response{ Success: false, Error: &ErrorInfo{Code: code, Message: message}, })}
func main() { r := gin.Default()
r.GET("/api/users/:id", func(c *gin.Context) { id := c.Param("id") // Simulate a lookup if id == "0" { Fail(c, http.StatusNotFound, "USER_NOT_FOUND", "no user with that ID") return } OK(c, gin.H{"id": id, "name": "Alice"}) })
r.Run(":8080")}صفحهبندی
صفحهبندی Limit/Offset
صفحهبندی Limit/Offset سادهترین رویکرد است. برای مجموعه دادههای کوچک تا متوسط که محاسبه تعداد کل ردیفها مقرون به صرفه است، به خوبی کار میکند.
package main
import ( "net/http" "strconv"
"github.com/gin-gonic/gin")
func main() { r := gin.Default()
r.GET("/api/articles", func(c *gin.Context) { limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
if limit > 100 { limit = 100 // cap the page size }
// articles, total := db.ListArticles(limit, offset)
c.JSON(http.StatusOK, gin.H{ "success": true, "data": []gin.H{}, // articles "meta": gin.H{ "limit": limit, "offset": offset, "total": 0, // total }, }) })
r.Run(":8080")}صفحهبندی مبتنی بر مکاننما
صفحهبندی مبتنی بر مکاننما از مشکلات عملکردی offsetهای بزرگ جلوگیری میکند. شناسه آخرین آیتم (یا فیلد منحصربهفرد و قابل مرتبسازی دیگر) را به عنوان مکاننما ارسال کنید.
package main
import ( "net/http" "strconv"
"github.com/gin-gonic/gin")
func main() { r := gin.Default()
r.GET("/api/events", func(c *gin.Context) { cursor := c.Query("cursor") // e.g. last event ID limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
if limit > 100 { limit = 100 }
// events, nextCursor := db.ListEvents(cursor, limit) _ = cursor
c.JSON(http.StatusOK, gin.H{ "success": true, "data": []gin.H{}, // events "next_cursor": "", // nextCursor (empty string means no more pages) }) })
r.Run(":8080")}فیلتر و مرتبسازی
فیلتر و مرتبسازی را از طریق پارامترهای پرسوجو بپذیرید. رابط را با استفاده از نامهای پارامتر سازگار قابل پیشبینی نگه دارید.
package main
import ( "net/http"
"github.com/gin-gonic/gin")
func main() { r := gin.Default()
// GET /api/products?category=electronics&min_price=10&sort=price&order=asc r.GET("/api/products", func(c *gin.Context) { category := c.Query("category") minPrice := c.Query("min_price") maxPrice := c.Query("max_price") sortBy := c.DefaultQuery("sort", "created_at") order := c.DefaultQuery("order", "desc")
// Validate the sort field against an allow-list to prevent injection. allowed := map[string]bool{"created_at": true, "price": true, "name": true} if !allowed[sortBy] { sortBy = "created_at" } if order != "asc" && order != "desc" { order = "desc" }
// Build and execute your query using these filters... _ = category _ = minPrice _ = maxPrice
c.JSON(http.StatusOK, gin.H{ "success": true, "data": []gin.H{}, "filters": gin.H{ "category": category, "min_price": minPrice, "max_price": maxPrice, "sort": sortBy, "order": order, }, }) })
r.Run(":8080")}نسخهبندی API
نسخهبندی مسیر URL
نسخهبندی مسیر URL رایجترین استراتژی است. صریح است، مسیریابی آسان دارد و تست با curl ساده است.
package main
import ( "net/http"
"github.com/gin-gonic/gin")
func main() { r := gin.Default()
v1 := r.Group("/api/v1") { v1.GET("/users", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": "v1", "users": []string{}}) }) }
v2 := r.Group("/api/v2") { v2.GET("/users", func(c *gin.Context) { // v2 returns a different shape c.JSON(http.StatusOK, gin.H{ "version": "v2", "data": []gin.H{}, "meta": gin.H{"total": 0}, }) }) }
r.Run(":8080")}نسخهبندی مبتنی بر هدر
نسخهبندی هدر URLها را تمیز نگه میدارد اما نیاز دارد کلاینتها یک هدر سفارشی تنظیم کنند. یک میانافزار میتواند هدر را بخواند و نسخه را در context ذخیره کند.
package main
import ( "net/http"
"github.com/gin-gonic/gin")
// VersionMiddleware reads the API version from the Accept-Version header.func VersionMiddleware() gin.HandlerFunc { return func(c *gin.Context) { version := c.GetHeader("Accept-Version") if version == "" { version = "v1" // default } c.Set("api_version", version) c.Next() }}
func main() { r := gin.Default() r.Use(VersionMiddleware())
r.GET("/api/users", func(c *gin.Context) { version := c.GetString("api_version")
switch version { case "v2": c.JSON(http.StatusOK, gin.H{"version": "v2", "data": []gin.H{}}) default: c.JSON(http.StatusOK, gin.H{"version": "v1", "users": []string{}}) } })
r.Run(":8080")}الگوهای مدیریت خطا
انواع خطای سفارشی
انواع خطای سطح برنامه را تعریف کنید تا handlerها بتوانند خطاهای معنادار و ساختارمند برگردانند.
package main
import ( "errors" "net/http"
"github.com/gin-gonic/gin")
// AppError represents a structured API error.type AppError struct { Status int `json:"-"` Code string `json:"code"` Message string `json:"message"`}
func (e *AppError) Error() string { return e.Message}
var ( ErrNotFound = &AppError{Status: 404, Code: "NOT_FOUND", Message: "resource not found"} ErrUnauthorized = &AppError{Status: 401, Code: "UNAUTHORIZED", Message: "authentication required"} ErrBadRequest = &AppError{Status: 400, Code: "BAD_REQUEST", Message: "invalid request"})
// ErrorHandler is a middleware that catches errors set via c.Error().func ErrorHandler() gin.HandlerFunc { return func(c *gin.Context) { c.Next()
if len(c.Errors) == 0 { return }
err := c.Errors.Last().Err var appErr *AppError if errors.As(err, &appErr) { c.JSON(appErr.Status, gin.H{ "success": false, "error": gin.H{"code": appErr.Code, "message": appErr.Message}, }) } else { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": gin.H{"code": "INTERNAL", "message": "an unexpected error occurred"}, }) } }}
func main() { r := gin.Default() r.Use(ErrorHandler())
r.GET("/api/items/:id", func(c *gin.Context) { id := c.Param("id") if id == "0" { _ = c.Error(ErrNotFound) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"id": id}}) })
r.Run(":8080")}سازماندهی مسیرها بر اساس منبع
با رشد API شما، مسیرها را بر اساس منبع سازماندهی کنید. هر منبع فایل خود را با تابعی که مسیرها را روی gin.RouterGroup ثبت میکند، دارد.
package main
import ( "net/http"
"github.com/gin-gonic/gin")
// RegisterUserRoutes sets up all /users endpoints.func RegisterUserRoutes(rg *gin.RouterGroup) { users := rg.Group("/users") { users.GET("/", listUsers) users.POST("/", createUser) users.GET("/:id", getUser) users.PUT("/:id", updateUser) users.DELETE("/:id", deleteUser) }}
// RegisterOrderRoutes sets up all /orders endpoints.func RegisterOrderRoutes(rg *gin.RouterGroup) { orders := rg.Group("/orders") { orders.GET("/", listOrders) orders.POST("/", createOrder) orders.GET("/:id", getOrder) }}
func listUsers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"action": "list_users"}) }func createUser(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"action": "create_user"}) }func getUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"action": "get_user"}) }func updateUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"action": "update_user"}) }func deleteUser(c *gin.Context) { c.Status(http.StatusNoContent) }
func listOrders(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"action": "list_orders"}) }func createOrder(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"action": "create_order"}) }func getOrder(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"action": "get_order"}) }
func main() { r := gin.Default()
api := r.Group("/api/v1") RegisterUserRoutes(api) RegisterOrderRoutes(api)
r.Run(":8080")}در یک پروژه واقعی، RegisterUserRoutes را در فایل routes/users.go و RegisterOrderRoutes را در routes/orders.go قرار میدهید، سپس هر دو را از main.go فراخوانی میکنید. این کار هر منبع را مستقل نگه میدارد و اضافه یا حذف منابع را بدون تغییر کد غیرمرتبط آسان میکند.