使用Golang建立RESTful API的最佳實踐案例
以下建立一個 REST API 應用的最佳實踐
庫包:
- Gin for HTTP
- gorm for ORM
- viper for configuration
- zap for logging
- testify for testing
- go2hal for HAL
- problem for problem JSON
- validator for validation
- sqlmock for SQL mocking
模型
使用ORM模型,在本例中,gorm使用該模型將結構轉換為 SQL 語句。例如:
type Workspace struct { ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4()" json:"id"` Name string `gorm:"not null,type:text" json:"name"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index,->" json:"-"` } |
- ID用作主鍵,使用隨機 UUID 而不是自增整數。
- name是模型的一個屬性,它可以是任何名稱下的任何有限數量的東西。
- 建立模型時的CreatedAt由 gorm 自動處理。
- UpdatedAt模型更新時,由 gorm 自動處理。
- DeletedAt這是 gorm 處理軟刪除的方式。它需要是 gorm.DeletedAt 的型別
儲存庫
儲存庫是一種設計模式,可以幫助我們進行CRUD操作。
我們先定義一個介面:
type Repository interface { Configure(*gorm.DB) List(after time.Time, limit int) (any, error) Get(id any) (any, error) Create(entity any) (any, error) Update(id any, entity any) (bool, error) Delete(id any) (bool, error) } |
然後他們使用 gorm 作為 ORM 的實現:
func (repository *WorkspaceRepository) List(after time.Time, limit int) (any, error) { var wc model.WorkspaceCollection order := "created_at" err := r.db.Limit(limit).Order(order).Where(fmt.Sprintf("%v > ?", order), after).Limit(limit).Find(&wc).Error return wc, err } func (repository *WorkspaceRepository) Get(id any) (any, error) { var w *model.Workspace err := r.db.Where("id = ?", id).First(&w).Error return w, err } func (repository *WorkspaceRepository) Create(entity any) (any, error) { w := entity.(*model.Workspace) err := r.db.Create(w).Error return w, err } func (repository *WorkspaceRepository) Update(id any, entity any) (bool, error) { w := entity.(*model.Workspace) if err := r.db.Model(w).Where("id = ?", id).Updates(w).Error; err != nil { return false, err } return true, nil } func (repository *WorkspaceRepository) Delete(id any) (bool, error) { if err := r.db.Delete(&model.Workspace{}, "id = ?", id).Error; err != nil { return false, err } return true, nil } |
路由
為了處理 HTTP 路由,我們需要建立一些稱為控制器的函式,在這個例子中,使用了Gin。
func (server *Server) registerRoutes() { var router = server.router workspaces := router.Group("/workspaces") { workspaces.GET("", GetWorkspaces) workspaces.POST("", CreateWorkspace) workspaces.GET("/:uuid", GetWorkspace) workspaces.PATCH("/:uuid", UpdateWorkspace) workspaces.DELETE("/:uuid", DeleteWorkspace) } } |
控制器
控制器負責處理 HTTP 呼叫並返回有用的資訊,這些資訊可以是帶有來自 ORM 的物件的 JSON 或錯誤。讓我們實現所有的 CRUD 操作:
func GetWorkspaceRepository(ctx *gin.Context) repository.Repository { return ctx.MustGet("RepositoryRegistry").(*repository.RepositoryRegistry).MustRepository("WorkspaceRepository") } func GetWorkspaces(ctx *gin.Context) { var q = query{} if err := ctx.ShouldBindQuery(&q); err != nil { HandleError(err, ctx) return } entities, err := GetWorkspaceRepository(ctx).List(q.After, q.Limit) if err != nil { HandleError(err, ctx) return } WriteHAL(ctx, http.StatusOK, entities.(model.WorkspaceCollection).ToHAL(ctx.Request.URL.Path, ctx.Request.URL.Query())) } func GetWorkspace(ctx *gin.Context) { p := params{} ctx.ShouldBindUri(&p) if err := validate.Struct(p); err != nil { HandleError(err, ctx) return } entity, err := GetWorkspaceRepository(ctx).Get(p.ID) if err != nil { HandleError(err, ctx) return } WriteHAL(ctx, http.StatusOK, entity.(*model.Workspace).ToHAL(ctx.Request.URL.Path)) } func CreateWorkspace(ctx *gin.Context) { body := model.Workspace{} if err := ctx.BindJSON(&body); err != nil { HandleError(err, ctx) return } entity, err := GetWorkspaceRepository(ctx).Create(&body) if err != nil { HandleError(err, ctx) return } workspace := entity.(*model.Workspace) selfHref, _ := url.JoinPath(ctx.Request.URL.Path, workspace.ID.String()) WriteHAL(ctx, http.StatusCreated, workspace.ToHAL(selfHref)) } func UpdateWorkspace(ctx *gin.Context) { p := params{} ctx.ShouldBindUri(&p) if err := validate.Struct(p); err != nil { HandleError(err, ctx) return } body := model.Workspace{} if err := ctx.BindJSON(&body); err != nil { HandleError(err, ctx) return } repository := GetWorkspaceRepository(ctx) _, err := repository.Update(p.ID, &body) if err != nil { HandleError(err, ctx) return } entity, err := repository.Get(p.ID) if err != nil { HandleError(err, ctx) return } WriteHAL(ctx, http.StatusOK, entity.(*model.Workspace).ToHAL(ctx.Request.URL.Path)) } func DeleteWorkspace(ctx *gin.Context) { p := params{} ctx.ShouldBindUri(&p) if err := validate.Struct(p); err != nil { HandleError(err, ctx) return } _, err := GetWorkspaceRepository(ctx).Delete(p.ID) if err != nil { HandleError(err, ctx) return } WriteNoContent(ctx) } |
HAL 連結
API 是永恆的。一旦將 API 整合到生產應用程式中,就很難進行可能破壞現有整合的重大更改
Web API 設計原則:使用 API 和微服務交付價值
在實踐中,很難打破 API 契約,因為 API 使用者會生你的氣。新版本的 API 不實用;沒有人會轉移到另一個 API。
考慮到這一點,正式名稱為 JSON 超文字應用程式語言的HAL Links嘗試以一種沒有痛苦的方式解決 API 遷移。API 應該在self欄位中返回資源的表示,而不是對資源使用硬編碼的位置。
{ "_links": { "self": "/workspaces/6424f2b7-8094-48de-a68c-24bbb7de1faa" } } ... |
實現非常簡單:
func (model *Workspace) ToHAL(selfHref string) (root hal.Resource) { root = hal.NewResourceObject() root.AddData(model) selfRel := hal.NewSelfLinkRelation() selfLink := &hal.LinkObject{Href: selfHref} selfRel.SetLink(selfLink) root.AddLink(selfRel) return } |
問題
你可能已經注意到每個錯誤都會呼叫HandleError函式,這個函式負責透過返回application/problem+json將錯誤變成更有意義的東西。
func HandleError(err error, ctx *gin.Context) { var p *problem.Problem switch { case errors.Is(err, gorm.ErrRecordNotFound): p = problem.New( problem.Title("Record Not Found"), problem.Type("errors:database/record-not-found"), problem.Detail(err.Error()), problem.Status(http.StatusNotFound), ) break default: p = problem.New( problem.Title("Bad Request"), problem.Type("errors:http/bad-request"), problem.Detail(err.Error()), problem.Status(http.StatusBadRequest), ) break } p.WriteTo(ctx.Writer) } |
例如,如果after引數不是RFC 3339的格式。它將返回一個錯誤
$ http localhost:8000/workspaces?after=0 HTTP/1.1 400 Bad Request Content-Length: 164 Content-Type: application/problem+json Date: Sat, 30 Jul 2022 18:23:54 GMT { "detail": "parsing time \Ŕ\" as \?-01-02T15:04:05Z07:00\": cannot parse \Ŕ\" as \?\"", "status": 400, "title": "Bad Request", "type": "errors:http/bad-request" } |
注意Content-Type,它是HTTP APIs的問題細節的mimetype,正文中有一個詳細的錯誤。
相關文章
- restful api最佳實踐RESTAPI
- RESTful API 最佳實踐RESTAPI
- RESTful API 設計指南——最佳實踐RESTAPI
- 我所認為的RESTful API最佳實踐RESTAPI
- Koa2+MongoDB+JWT實戰--Restful API最佳實踐MongoDBJWTRESTAPI
- RESTful API實踐總結RESTAPI
- 使用Golang和MongoDB構建 RESTful APIGolangMongoDBRESTAPI
- 使用Android API最佳實踐AndroidAPI
- 【GoLang】golang 最佳實踐彙總Golang
- 理解RESTful:理論與最佳實踐REST
- RESTful API 設計思路及實踐RESTAPI
- 使用 Flask 實現 RESTful APIFlaskRESTAPI
- SAP Marketing Cloud Restful API SDK 使用案例分享CloudRESTAPI
- Django RESTful API設計與實踐指南DjangoRESTAPI
- Google:12 條 Golang 最佳實踐Golang
- Golang效能最佳化實踐Golang
- 使用 Wake Lock API:保持裝置喚醒的最佳實踐API
- 7個API安全最佳實踐API
- 探討Morest在RESTful API測試的行業實踐RESTAPI行業
- 基於 SpringMVC 的 RESTful HTTP API 實踐(服務端)SpringMVCRESTHTTPAPI服務端
- 綜合Twitter、Github等各大網站API設計經驗:RESTful API實用設計與最佳實踐 - Vinay SahniGithub網站APIREST
- 建立現代npm包的最佳實踐NPM
- 《Golang學習筆記》error最佳實踐Golang筆記Error
- 使用Rust+Rocket建立一個CRUD的RESTful歌曲請求APIRustRESTAPI
- 13 個設計 REST API 的最佳實踐RESTAPI
- Go語言RESTful JSON API建立GoRESTJSONAPI
- .NET雲原生應用實踐(二):Sticker微服務RESTful API的實現微服務RESTAPI
- JavaScript 建立物件模式與最佳實踐JavaScript物件模式
- Java Optional使用的最佳實踐Java
- 使用Kotlin + Jersey + Jetty + MongoDB建立可擴充套件的RESTful API - AndrewKotlinJettyMongoDB套件RESTAPI
- 電商API介面的實踐與案例分析API
- 淘寶API介面呼叫:案例分析與實踐API
- DRF使用超連結API實現真正RESTfulAPIREST
- 智慧客服API最佳實踐—智慧物流客服API
- Android開發中API層的最佳實踐AndroidAPI
- nodejs實現restful APINodeJSRESTAPI
- 使用GitHub的十個最佳實踐Github
- 設計出色API的最佳實踐與原則 - JamesAPI