1. 程式人生 > >Golang Web入門(4):如何設計API

Golang Web入門(4):如何設計API

## 摘要 在之前的幾篇文章中,我們從如何實現最簡單的HTTP伺服器,到如何對路由進行改進,到如何增加中介軟體。總的來講,我們已經把Web伺服器相關的內容大概梳理了一遍了。在這一篇文章中,我們將從最簡單的一個main函式開始,慢慢重構,來研究如何把API設計的更加規範和具有擴充套件性。 ## 1 構建一個Web應用 我們從最簡單的開始,利用`gin`框架實現一個小應用。 在這這篇文章中,我先不使用`MySQL`和`Redis`,快取和持久化相關的內容我將在以後的文章中提到。在這個系列中,我們主要還是聊聊與Web有關的內容。 ``` package main import ( "github.com/gin-gonic/gin" "net/http" ) type Result struct { Success bool Msg string } func Login (ctx *gin.Context) { username := ctx.PostForm("username") password := ctx.PostForm("password") //這裡判斷使用者名稱密碼的正確性 r := Result{false, "請求失敗"} if username != "" && password != "" { r = Result{true, "請求成功"} } ctx.JSON(http.StatusOK, r) } func main() { router := gin.New() router.Use(gin.Logger(), gin.Recovery()) router.POST("/login", Login) router.Run(":8000") } ``` 這是一個簡單到不能再簡單的登入介面了。請求之後的返回的結果如下: ``` { "Success": true, "Msg": "請求成功" } ``` 在這個`Handler`中的邏輯是這樣的:獲取`POST`請求中的`body`引數,得到了使用者傳到後臺的使用者名稱和密碼。 然後應該在資料庫中進行比對,**在這裡省略了這一步驟**。 我們建立了一個結構體,作為**返回的JSON結構**。 最後呼叫了`gin`的JSON方法返回資料。這裡的第一個引數是**HTTP狀態碼**,第二個引數是需要**返回的資料**。我們來看看這個JSON方法: ``` // JSON serializes the given struct as JSON into the response body. // It also sets the Content-Type as "application/json". func (c *Context) JSON(code int, obj interface{}) { c.Render(code, render.JSON{Data: obj}) } ``` 意思是,會把返回的資料序列化為JSON型別,並且把`Content-Type`設定為`application/json`。 ```! 注意,如果這裡你的結構體欄位第一個字母是小寫,返回的json資料將為空。原因是這樣的,這裡呼叫了別的包的序列化方法,如果是小寫的欄位,在別的包無法訪問,也就會造成返回資料為空的情況。 ``` 但是你有沒有發現,把全部業務邏輯都丟到`main`函式的做法簡直太不優雅了!所有的業務邏輯都耦合在一起,沒有做到“**一個函式實現一個功能**”的目標。 好,下面我們開始**重構**。 ## 2 Handler 既然所有的函式都在`main`函式中,我們不如先把`Handler`轉移出來,單獨作為一個包。 ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152335197-681642564.png) 這時候我們來看看`main`函式: ``` package main import ( "github.com/gin-gonic/gin" "hongjijun.com/helloworldGo/api/v1" ) func main() { router := gin.New() router.Use(gin.Logger(), gin.Recovery()) router.POST("/login", v1.Login) router.Run(":8080") } ``` 是不是感覺已經好很多了。 在`main`函式中,主要就是註冊路由,而其餘的`Handler`,則儲存在其他的包中。 我們繼續看看我們的`Handler`: ``` package v1 import ( "github.com/gin-gonic/gin" "net/http" ) type Result struct { Success bool Msg string } func Login(ctx *gin.Context) { username := ctx.PostForm("username") password := ctx.PostForm("password") //這裡判斷使用者名稱密碼的正確性 r := Result{false, "請求失敗"} if username != "" && password != "" { r = Result{true, "請求成功"} } ctx.JSON(http.StatusOK, r) } ``` 在這裡我們發現這個包的程式碼還是不夠**整潔**。 為什麼呢,因為我們把返回結果也放到了這個包中。而返回結果,他應該是通用的。 **既然是通用的,那我們就應該把它抽象出來。** ## 3 Response 我們來看看此時包的結構: ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152352061-475393519.png) 我們新建了一個名為`common`的目錄。在這個目錄中我們將存放一些專案的公共資源。 來看看我們抽象出的response: ``` package response import ( "github.com/gin-gonic/gin" "net/http" ) type Result struct { Success bool Code int Msg string Data interface{} } func response(success bool, code int, msg string, data interface{}, ctx *gin.Context) { r := Result{success, code, msg, data} ctx.JSON(http.StatusOK, r) } func successResponse(data interface{}, ctx *gin.Context) { response(true, 0, "請求成功", data, ctx) } func failResponse(code int, msg string, ctx *gin.Context) { response(false, code, msg, nil, ctx) } func SuccessResultWithEmptyData(ctx *gin.Context) { successResponse(nil, ctx) } func SuccessResult(data interface{}, ctx *gin.Context) { successResponse(data, ctx) } func FailResultWithDefaultMsg(code int, ctx *gin.Context) { failResponse(code, "請求失敗", ctx) } func FailResult(code int, msg string, ctx *gin.Context) { failResponse(code, msg, ctx) } ``` 簡單來講,就是設定了**請求成功**和**請求錯誤**的返回結果。在請求成功的返回結果中,有**不返回資料**的空結果以及**返回了一些查詢資料**的結果。在失敗的結果中,有**預設**的結果,和**帶具體資訊**的結果。 這些需要按照實際的情況來處理,這裡只是做個示範。 注意,因為在返回的結果中,成功的結果`success`為`true`,`code`為`0`,而失敗的結果`success`為`false`,`code`需要按照專案的規劃來設定,所以作者在這裡**又做了一層抽象**,設定了`successResponse`和`failResponse函式`。 而這兩個函式都會呼叫`gin`上下文中的`JSON`方法,所以將這裡的返回再次抽象,抽象出了`response`函式。 ```! 注意,在這個response包中,只有返回結果的幾個函式:SuccessResultWithEmptyData、SuccessResult、FailResultWithDefaultMsg、FailResult是給外部函式呼叫的,其他的函式是內部呼叫的。所以注意函式名第一個字母的大小寫,來設定公有還是私有。 ``` 如圖: ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152407005-761412039.png) **其餘的任何函式,在外部都是無法呼叫的。** 此時,我們再來看看**Handler**: ``` package v1 import ( "github.com/gin-gonic/gin" "hongjijun.com/helloworldGo/common" ) func Login(ctx *gin.Context) { username := ctx.PostForm("username") password := ctx.PostForm("password") //這裡判斷使用者名稱密碼的正確性 if username != "" && password != ""{ response.SuccessResultWithEmptyData(ctx) } } ``` 此時,無論在哪個Handler中,我們**只需要**呼叫response.Xxx,就能返回資料了。 到了這裡,Handler部分基本上講完了。但是作者在這裡還沒有實現對錯誤結果的抽象,你可以自己試試看。 ## 4 服務啟動 現在我們的main函式雖然比起之前簡潔了不少: ``` func main() { router := gin.New() router.Use(gin.Logger(), gin.Recovery()) router.POST("/login", v1.Login) router.Run(":8080") } ``` 但是,看起來整潔只是因為這裡**只有一個**路由。 想象一下如果我們有了很多個路由,那這裡還是會變成一大串,所以我們要對這個`main`函式進行重構。 我們直接新建一個名為`run.go`的檔案(借鑑了Spring boot的結構)。 ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152425909-242447592.png) 這個`run.go`的程式碼,就是原來`main`函式裡面的程式碼: ``` package application import ( "github.com/gin-gonic/gin" v1 "hongjijun.com/helloworldGo/api/v1" ) func Run() { router := gin.New() router.Use(gin.Logger(), gin.Recovery()) router.POST("/login", v1.Login) router.Run(":8080") } ``` 因此,`main`函式變成了這樣: ``` package main import ( "hongjijun.com/helloworldGo/application" ) func main() { application.Run() } ``` 真的是越來越像Spring boot了(笑) 這樣子的話,我們的應用入口就顯得很簡潔了。但是在Run函式中,依舊沒有解決我們說的當路由增加之後的複雜性,我們繼續往下重構。 ## 5 Router 我們來想一想,在`Run()`這個函式中,是為了啟動服務。這裡說的服務,不僅僅是指現在在操作的路由,還有其他的服務,比如資料庫連線池,Redis等等。 所以,我們應該把路由部分的服務抽象出來。 我們之間來看看效果: ``` package application import ( "hongjijun.com/helloworldGo/application/initial" ) func Run() { router := initial.Router() // 這裡還可以建立其他的服務 // ... router.Run(":8080") } ``` 注意看,我們的路由處理,已經被挪到了其他位置了。在這個`Run()`函式中,我們只需要獲取路由,然後執行,別的操作,**不應該由這個函式來完成**。 然後我們再來看看`initial.Router()`這個函式。 ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152444069-598605232.png) 注意看,我在`application`這個目錄下,新建了一個叫`initial`的目錄,這個`initial`目錄和我們的`run.go`是同級的。 我們來看看`router.go`: ``` package initial import ( "github.com/gin-gonic/gin" "hongjijun.com/helloworldGo/router" ) func Router() *gin.Engine{ //新建一個路由 router := gin.New() //註冊中介軟體 router.Use(gin.Logger(), gin.Recovery()) //設定一個分組,這裡的分組是空的,是為了之後進行更細緻的分組 api := router.Group("") //加入使用者管理類的路由 apirouter.InitMangerUserRouter(api) // ...插入其他的路由 //返回 return router } ``` 很容易理解,在這個Router()方法中,定義了中介軟體,路由分組這些東西。 這裡先解釋一下: 我們先設定了一個空的路由分組,這個分組是作為**根分組**存在的。然後,我們把各個模組作為這個分組的**子分組**。舉個例子:我們的專案中,有使用者相關的模組,有訂單相關的模組,那麼這裡的一個模組,就是一個分組,一個分組下面,有多個介面。 ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152458526-1127197385.png) 所以,我們就可以組成這些路由: - /manageUser/register - /manageUser/login - /order/add - /order/delete 所以,我們增加這樣的目錄: ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152507611-529972100.png) 所有的分組,都放在`router`這個檔案目錄下。 然後我們再看看`apirouter.InitMangerUserRouter(api)`這個方法,這個方法就是增加`/manageUser/*`的一些路由。這個方法存在於上文提到的`router`這個目錄中: ``` package apirouter import ( "github.com/gin-gonic/gin" v1 "hongjijun.com/helloworldGo/api/v1" ) func InitMangerUserRouter(group *gin.RouterGroup) { manageUserRouter := group.Group("manageUser") manageUserRouter.POST("login", v1.Login) // ...其他路由 } ``` 在這個註冊路由分組的函式中,我們先把分組設定為`manageUser`,表示下面的路由都會拼接在`manageUser`後面。 然後,我們在這裡註冊了`login`,並且,在這裡還可以繼續寫屬於`manageUser`這個模組的其他路由。 ## 6 整體檔案結構 ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152522483-1901870437.png) - api目錄:所有的Handler - application目錄:應用所需的各種服務,如路由,持久化,快取等等,然後由run.go統一啟動 - common目錄:公共資源,如抽象的返回結果等 - router目錄:註冊各種路由分組 - main.go:啟動應用 ## 7 寫在最後 首先,謝謝你能看到這裡~ 在這一篇的文章中,我主要是總結了前面三篇文章的內容,構建了一個Web應用的Demo。這裡面很多都是我自己對於Web應用結構的理解,不一定對,也不一定合適,主要是做一個示範,希望能夠對你的學習起到一些啟發啟發作用。也希望你可以指出我的錯誤,我們一起進步~ 到了這裡,《Golang Web入門》系列就結束了,謝謝你們的支援。之前你們的關注和點贊,都是對我特別大的鼓勵。也非常感謝你們在發現了錯誤之後的留言,讓我知道了自己理解有誤的地方。(鞠躬~ PS:如果有其他的問題,也可以在公眾號找到作者。並且,所有文章第一時間會在公眾號更新,歡迎來找作者玩~ ![](https://img2020.cnblogs.com/blog/1998080/202004/1998080-20200426152537898-3448946