摘要
在之前的幾篇文章中,我們從如何實現最簡單的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:如果有其他的問題,也可以在公眾號找到作者。並且,所有文章第一時間會在公眾號更新,歡迎來找作者玩~

