Golang Web入門(4):如何設計API
- 2020 年 4 月 26 日
- 筆記
- Golang學習筆記
摘要
在之前的幾篇文章中,我們從如何實現最簡單的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
轉移出來,單獨作為一個包。
這時候我們來看看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
我們來看看此時包的結構:
我們新建了一個名為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是給外部函數調用的,其他的函數是內部調用的。所以注意函數名第一個字母的大小寫,來設置公有還是私有。
如圖:
其餘的任何函數,在外部都是無法調用的。
此時,我們再來看看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的結構)。
這個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()
這個函數。
注意看,我在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()方法中,定義了中間件,路由分組這些東西。
這裡先解釋一下:
我們先設置了一個空的路由分組,這個分組是作為根分組存在的。然後,我們把各個模塊作為這個分組的子分組。舉個例子:我們的項目中,有用戶相關的模塊,有訂單相關的模塊,那麼這裡的一個模塊,就是一個分組,一個分組下面,有多個接口。
所以,我們就可以組成這些路由:
- /manageUser/register
- /manageUser/login
- /order/add
- /order/delete
所以,我們增加這樣的目錄:
所有的分組,都放在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 整體文件結構
- api目錄:所有的Handler
- application目錄:應用所需的各種服務,如路由,持久化,緩存等等,然後由run.go統一啟動
- common目錄:公共資源,如抽象的返回結果等
- router目錄:註冊各種路由分組
- main.go:啟動應用
7 寫在最後
首先,謝謝你能看到這裡~
在這一篇的文章中,我主要是總結了前面三篇文章的內容,構建了一個Web應用的Demo。這裏面很多都是我自己對於Web應用結構的理解,不一定對,也不一定合適,主要是做一個示範,希望能夠對你的學習起到一些啟發啟發作用。也希望你可以指出我的錯誤,我們一起進步~
到了這裡,《Golang Web入門》系列就結束了,謝謝你們的支持。之前你們的關注和點贊,都是對我特別大的鼓勵。也非常感謝你們在發現了錯誤之後的留言,讓我知道了自己理解有誤的地方。(鞠躬~
PS:如果有其他的問題,也可以在公眾號找到作者。並且,所有文章第一時間會在公眾號更新,歡迎來找作者玩~