此前一直寫java,最近轉go了,總結一下如何用Go語言開發RESTful API接口服務,希望對Go新手有所幫助,同時也希望Go大神不吝賜教!
Frameworks and Libraries
Gin
網絡框架,采用的Gin。Gin 是用 Go 編寫的一個 Web 應用框架,對比其它主流的同類框架,他有更好的性能和更快的路由。由于其本身只是在官方 net/http 包的基礎上做的完善,所以理解和上手很平滑。
Xorm
對數據庫操作,我們可以直接寫SQl,也可以使用ORM工具,因為ORM工具使用起來方便簡潔。我們項目中使用的是xorm庫。特點是提供簡單但豐富實用的 API 來完成對數據庫的各類操作。該庫支持包括 MySQL、PostgreSQL、SQLite3 和 MsSQL 在內的主流數據庫,其在支持鏈式操作的基礎上,還允許結合SQL語句進行混合處理。另外,該庫還支持session事務和回滾以及樂觀鎖等。
Project and Package Structure
本項目是一個服務的QUERY端,因為我們采用的CQRS。因為query端簡單,因此采用三層結構。分別為Controller層,Apllication層,以及Gateway層。接下來詳細了解各層職責。
Controller層
Controller層,主要提供Restful接口,Restful接口一定要符合規范,這樣,別人才容易理解。
本接口是要獲取我的crm系統中,某個商戶的某個需求的詳細信息。因此Restful接口設計如下:
func RequirementRouter(r *gin.Engine) {
r.GET("crm/merchants/:merchantId/requirements/:requirementId", handler.QueryRequirementById)
}
Application層
Application層是主要邏輯層,需要完成權限校驗和數據查詢以及格式轉換的功能。其中分為了三個Package,分別為Auth,handler以及view。
auth
Auth package中提供身份認證以及權限校驗接口。
// Get params from request and to authenticate
func HandleAuthenticateRequest() gin.HandlerFunc {
return func(c *gin.Context) {
userId := c.GetHeader(configuration.UIN)
log.Debug("HandleAuthenticateRequest,user id is ", userId)
identity, err := GetCasBinAuthInstance().HandleAuthenticate(userId, nil)
if identity == "" || err != nil {
c.Status(http.StatusUnauthorized)
c.Abort()
}
c.Next()
}
}
// Get params from request and to authorize
func HandleAuthorizeRequest(ctx *gin.Context, objectType string, operation string) error {
log.Debug("HandleAuthorizeRequest, object and operation is ", objectType, operation)
userId := getUserIdFromRequest(ctx)
if userId == "" {
return errs.New(errs.UNAUTHORIZED, "user authorize failed,can not get user id")
}
merchantId := getMerchantIdFromRequest(ctx)
if merchantId == "" {
return errs.New(errs.UNAUTHORIZED, "user authorize failed,can not get merchant id")
}
permission := getPermission(objectType, operation, merchantId)
success, err := GetCasBinAuthInstance().HandleAuthorize(userId, permission)
if err != nil || !success {
log.Warn("user authorize failed, userId=", userId, ", err=", err)
return errs.New(errs.UNAUTHORIZED, "user authorize failed")
}
return nil
}
因為每個接口都需要身份認證,其實身份認證可以放到請求入口處做,而權限校驗不是每個接口都需要,同時,權限包含一定的業務邏輯,因此權限校驗在Applciation這一層做是比較合適的。
handler
handler pankage中是controller層和application的一個中間層,每一個Restful請求,對應一個Gin 的HandlerFunc,看一個Gin中的請求接口定義:
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("POST", relativePath, handlers)
}
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("DELETE", relativePath, handlers)
}
在每個handler我們首先調用auth的權限校驗接口進行權限校驗,然后對必須的請求參數進行檢查,如果參數無效或缺少,返回錯誤,如果權限以及參數校驗均通過,則調用view中的真正邏輯接口,進行查詢數據。
func QueryRequirementById(ctx *gin.Context) {
err := auth.HandleAuthorizeRequest(ctx, "P", "VIEW_REQUIREMENT_DETAIL")
if err != nil {
responseBodyHandler(ctx, nil, err)
return
}
requirementId, err := strconv.Atoi(ctx.Param("requirementId"))
if err != nil {
resp := CreateResp("", errs.INVALID_PARAMETER, "請提供正確的需求ID")
ctx.JSON(http.StatusBadRequest, &resp)
return
}
doQueryRequirementById(ctx, requirementId)
}
func doQueryRequirementById(context *gin.Context, requirementId int) {
defer func() {
if err := recover(); err != nil {
resp := CreateResp("", errs.UNKNOWN_ERROR, "server internal error")
context.JSON(http.StatusInternalServerError, &resp)
log.Error("QueryRequirementById errors=", err)
}
}()
requirement, err := requirementView.QueryRequirementById(context, requirementId)
responseBodyHandler(context, requirement, err)
}
一般函數不宜函數過長,如太長,建議封裝為子函數。
view
view pankage中,是真正的查詢邏輯service層,從數據庫中查詢出數據,然后,進行協議轉換,返回給Controller。
// Query requirement by requirement id
func (v *RequirementView) QueryRequirementById(ctx context.Context, requirementId int) (*dto.RequirementDetail, error) {
requirementPo, err := v.requirementDao.QueryRequirement(requirementId)
if err != nil {
return nil, nil
}
requirement :=getRequirementFromPo(requirementPo)
return requirementDetail, nil
}
func (v *RequirementView) getRequirementFromPo(po *po.RequirementPo) *dto.RequirementDetail {
var requirementDetai = dto.RequirementDetail{
Id: po.Id,
Name: po.Name,
Category: categoryName,
Amount: po.Amount,
AmountUnit: po.AmountUnit,
ExpectedDeliveryDate: util.ToTimestamp(po.ExpectedDeliveryDate),
Description: po.Description,
UnitPrice: po.PricePerItem,
DeliveryRhythm: po.DeliveryRhythm,
DeliveryStandard: po.DeliveryStandard,
CheckPeriod: po.CheckPeriod,
}
return &requirementDetai
}
Gateway層
Gateway主要就是提供外部存儲的增刪改查能力,我們存儲采用的mysql,因此,gateway主要就是操作mysql數據庫。那么主要就包括Po以及Dao。
Xorm提供了很方便的工具,XORM工具,可以根據數據庫表結構,生成相應的Po。這個工具的使用文檔也可以參考這篇:使用xorm工具,根據數據庫自動生成go代碼
本項目生成的的RequirementPo如下:
type RequirementPo struct {
Id int `xorm:"not null pk comment('主鍵、自增') INT(11)"`
Name string `xorm:"not null default '' comment('需求名稱, 最大長度: 50') VARCHAR(100)"`
Description string `xorm:"not null comment('需求描述, 最大長度: 10000') LONGTEXT"`
Status int `xorm:"not null comment('0: 新需求 1:已啟動 2:生產中 3:已完成 4:結算中 5:已結算 6:已關閉') INT(11)"`
CategoryId int `xorm:"not null comment('t_horizon_requirement_category 表的對應Id') INT(11)"`
MerchantId string `xorm:"comment('CRM商戶ID') index VARCHAR(100)"`
Creator string `xorm:"not null default '' comment('需求創建人') VARCHAR(50)"`
PricePerItem float64 `xorm:"not null comment('需求單價') DOUBLE(16,2)"`
Amount float64 `xorm:"not null comment('需求數據量') DOUBLE(16,2)"`
AmountUnit string `xorm:"not null default '" "' comment('用途:需求數據量單位,取值范圍:無') VARCHAR(50)"`
ExpectedDeliveryDate time.Time `xorm:"not null default '1970-01-01 08:00:01' comment('期望交付日期') TIMESTAMP"`
DeliveryRule string `xorm:"not null comment('交付規則') VARCHAR(255)"`
DeliveryStandard string `xorm:"not null comment('交付標準, 最大長度: 10000') TEXT"`
DeliveryRhythm string `xorm:"not null default '" "' comment('用途:交付節奏;取值范圍:無') VARCHAR(500)"`
CheckPeriod string `xorm:"not null default '" "' comment('用途:驗收周期;取值范圍:無') VARCHAR(255)"`
DataVersion int `xorm:"default 0 comment('數據版本, 用于樂觀鎖') INT(11)"`
Channel string `xorm:"not null default '' comment('創建需求的渠道') VARCHAR(255)"`
CreateTime time.Time `xorm:"not null default '1970-01-01 08:00:01' comment('創建時間') TIMESTAMP"`
}
通過XORM可以很方方便的對數據Po進行增刪改查,我的查詢接口如下:
func (d *RequirementDao) QueryRequirement(requirementId int) (*po.RequirementPo, error) {
requirement := new(po.RequirementPo)
session := Requirement.Engine().NewSession()
_, err := session.Table("t_requirement").Where("id=?", requirementId).Get(requirement)
if err != nil {
log.Warn("query requirement error", err)
return nil, errs.New(errs.DB_OPERATION_FAILED, "query requirement info fail")
}
return requirement, nil
}
想要了解更多Xorm使用信息,可以查看其文檔:xorm Gobook 以及 xorm Godoc
main
最后看一下main package中的類文件職責。
func main() {
r := gin.New()
r.Use(gin.Recovery())
//添加監控
r.Use(MonitorHandler())
//添加身份認證校驗
r.use(authenticateHandler())
//restul接口路由
controller.RequirementRouter(r)
r.Run(":" + configuration.Base.Server.Port)
}
ok,這樣即一個go的http服務的主要模塊,當然還有配置文件類等,這個按照自己喜歡的方法,自行開發加載配置邏輯以及使用網上開源的包均可。
后記
剛接觸GO語言不久,還有很長的路要走,加油!