本篇博客主要介紹瞭如何從零開始,使用Go Module做爲依賴管理,基於Gin來一步一步搭建Go的Web服務器。並使用Endless來使服務器平滑重啓,使用Swagger來自動生成Api文檔。html
源碼在此處:項目源碼前端
你們能夠先查看源碼,而後再根據本篇文章,來了解搭建過程當中服務器的一些細節。java
如下全部的步驟都基於MacOS。node
在這裏推薦使用homebrew進行安裝。固然你也可使用源碼安裝。mysql
brew install go
跑完命令以後,在命令行輸入go
。若是在命令行看到以下輸出,則表明安裝成功。git
Go is a tool for managing Go source code. Usage: go <command> [arguments] The commands are: ... ...
須要注意的是,go的版本須要在1.11
之上,不然沒法使用go module。如下是個人go的版本。github
go version # go version go1.12.5 darwin/amd64
推薦使用GoLandgolang
打開GoLand,在GoLand的設置中找到Global GOPATH,將其設置爲$HOME/go
。$HOME
目錄就是你的電腦的用戶目錄,若是該目錄下沒有go
目錄的話,也不須要新建,當咱們在後面的操做中初始化模塊的時候,會自動的在用戶目錄下新建go目錄。spring
一樣,在GoLand中設置中找到Go Modules (vgo)。勾選Enable Go Modules (vgo) integration前的選擇框來啓用Go Moudlesql
在你經常使用的工做區新建一個目錄,若是你有github的項目,能夠直接clone下來。
go mod init $MODULE_NAME
在剛剛新建的項目的根目錄下,使用上述命令來初始化go module。該命令會在項目根目錄下新建一個go.mod的文件。
若是你的項目是從github上clone下來的,$MODULE_NAME
這個參數就不須要了。它會默認爲github.com/$GITHUB_USER_NAME/$PROJECT_NAME
。
例如本項目就是github.com/detectiveHLH/go-backend-starter
;若是是在本地新建的項目,則必需要加上最後一個參數。不然就會遇到以下的錯誤。
go: cannot determine module path for source directory /Users/hulunhao/Projects/go/test/src (outside GOPATH, no import comments)
初始化完成以後的go.mod
文件內容以下。
module github.com/detectiveHLH/go-backend-starter go 1.12
在項目的根目錄下新建main.go。代碼以下。
package main import ( "fmt" ) func main() { fmt.Println("This works") }
在根目錄下使用go run main.go
,若是看到命令行中輸出This works
則表明基礎的框架已經搭建完成。接下來咱們開始將Gin引入框架。
Gin是一個用Go實現的HTTP Web框架,咱們使用Gin來做爲starter的Base Framework。
直接經過go get命令來安裝
go get github.com/gin-gonic/gin
安裝成功以後,咱們能夠看到go.mod文件中的內容發生了變化。
而且,咱們在設定的GOPATH下,並無看到剛剛安裝的依賴。實際上,依賴安裝到了$GOPATH/pkg/mod下。
module github.com/detectiveHLH/go-backend-starter go 1.12 require github.com/gin-gonic/gin v1.4.0 // indirect
同時,也生成了一個go.sum文件。內容以下。
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
用過Node的人都知道,在安裝完依賴以後會生成一個package-lock.json文件,來鎖定依賴的版本。以防止後面從新安裝依賴時,安裝了新的版本,可是與現有的代碼不兼容,這會帶來一些沒必要要的BUG。
可是這個go.sum文件並非這個做用。咱們能夠看到go.mod中只記錄了一個Gin的依賴,而go.sum中則有很是多。是由於go.mod中只記錄了最頂層,就是咱們直接使用命令行安裝的依賴。可是要知道,一個開源的包一般都會依賴不少其餘的依賴包。
而go.sum就是記錄全部頂層和其中間接依賴的依賴包的特定版本的文件,爲每個依賴版本生成一個特定的哈希值,從而在一個新環境啓用該項目時,能夠作到對項目依賴的100%還原。go.sum還會保留一些過去使用過的版本的信息。
在go module下,不須要vendor目錄來保證可重現的構建,而是經過go.mod文件來對項目中的每個依賴進行精確的版本管理。
若是以前的項目用的是vendor,那麼從新用go.mod從新編寫不太現實。咱們可使用go mod vendor
命令將以前項目全部的依賴拷貝到vendor目錄下,爲了保證兼容性,在vendor目錄下的依賴並不像go.mod同樣。拷貝以後的目錄不包含版本號。
並且經過上面安裝gin能夠看出,一般狀況下,go.mod文件是不須要咱們手動編輯的,當咱們執行完命令以後,go.mod也會自動的更新相應的依賴和版本號。
下面咱們來了解一下go mod的相關命令。
還有一個命令值得提一下,go list -m all
能夠列出當前項目的構建列表。
修改main.go的代碼以下。
package main import ( "fmt" "github.com/gin-gonic/gin" ) func main() { fmt.Println("This works.") r := gin.Default() r.GET("/hello", func(c *gin.Context) { c.JSON(200, gin.H{ "success": true, "code": 200, "message": "This works", "data": nil, }) }) r.Run() }
上述的代碼引入了路由,熟悉Node的應該能夠看出,這個與koa-router的用法十分類似。
照着上述運行main.go的步驟,運行main.go。就能夠在控制檯看到以下的輸出。
This works. [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /hello --> main.main.func1 (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080
此時,服務器已經在8080端口啓動了。而後在瀏覽器中訪問http://localhost:8080/hello,就能夠看到服務器的正常返回。同時,服務器這邊也會打印相應的日誌。
[GIN] 2019/06/08 - 17:41:34 | 200 | 214.213µs | ::1 | GET /hello
在根目錄下新建router目錄。在router下,新建router.go文件,代碼以下。
package router import "github.com/gin-gonic/gin" func InitRouter() *gin.Engine { router := gin.New() apiVersionOne := router.Group("/api/v1/") apiVersionOne.GET("hello", func(c *gin.Context) { c.JSON(200, gin.H{ "success": true, "code": 200, "message": "This works", "data": nil, }) }) return router }
在這個文件中,導出了一個InitRouter函數,該函數返回gin.Engine類型。該函數還定義了一個路由爲/api/v1/hello的GET請求。
將main.go的代碼改成以下。
package main import ( "fmt" "github.com/detectiveHLH/go-backend-starter/router" ) func main() { r := router.InitRouter() r.Run() }
而後運行main.go,啓動以後,訪問http://localhost:8080/api/v1/hello,能夠看到,與以前訪問/hello路由的結果是同樣的。
到此爲止,咱們已經擁有了一個擁有簡單功能的Web服務器。那麼問題來了,這樣的一個開放的服務器,只要知道了地址,你的服務器就知道暴露給其餘人了。這樣會帶來一些安全隱患。因此咱們須要給接口加上鑑權,只有經過認證的調用方,纔有權限調用服務器接口。因此接下來,咱們須要引入JWT。
使用go get命令安裝jwt-go依賴。
go get github.com/dgrijalva/jwt-go
在根目錄下新建middleware/jwt目錄,在jwt目錄下新建jwt.go文件,代碼以下。
package jwt import ( "github.com/detectiveHLH/go-backend-starter/consts" "github.com/gin-gonic/gin" "net/http" "time" ) func Jwt() gin.HandlerFunc { return func(c *gin.Context) { var code int var data interface{} code = consts.SUCCESS token := c.Query("token") if token == "" { code = consts.INVALID_PARAMS } else { claims, err := util.ParseToken(token) if err != nil { code = consts.ERROR_AUTH_CHECK_TOKEN_FAIL } else if time.Now().Unix() > claims.ExpiresAt { code = consts.ERROR_AUTH_CHECK_TOKEN_TIMEOUT } } if code != consts.SUCCESS { c.JSON(http.StatusUnauthorized, gin.H{ "code": code, "msg": consts.GetMsg(code), "data": data, }) c.Abort() return } c.Next() } }
此時,代碼中會有錯誤,是由於咱們沒有聲明consts這個包,其中的變量SUCCESS、INVALID_PARAMS和ERROR_AUTH_CHECK_TOKEN_FAIL是未定義的。根據code獲取服務器返回信息的函數GetMsg也沒定義。一樣沒有定義的還有util.ParseToken(token)和claims.ExpiresAt。因此咱們要新建consts包。咱們在根目錄下新建consts目錄,而且在consts目錄下新建code.go,將定義好的一些常量引進去,代碼以下。
const ( SUCCESS = 200 ERROR = 500 INVALID_PARAMS = 400 )
再新建message.go文件,代碼以下。
var MsgFlags = map[int]string{ SUCCESS: "ok", ERROR: "fail", INVALID_PARAMS: "請求參數錯誤", } func GetMsg(code int) string { msg, ok := MsgFlags[code] if ok { return msg } return MsgFlags[ERROR] }
在根目錄下新建util,而且在util下新建jwt.go,代碼以下。
package util import ( "github.com/dgrijalva/jwt-go" "time" ) var jwtSecret = []byte(setting.AppSetting.JwtSecret) type Claims struct { Username string `json:"username"` Password string `json:"password"` jwt.StandardClaims } func GenerateToken(username, password string) (string, error) { nowTime := time.Now() expireTime := nowTime.Add(3 * time.Hour) claims := Claims{ username, password, jwt.StandardClaims { ExpiresAt : expireTime.Unix(), Issuer : "go-backend-starter", }, } tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token, err := tokenClaims.SignedString(jwtSecret) return token, err } func ParseToken(token string) (*Claims, error) { tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { return jwtSecret, nil }) if tokenClaims != nil { if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { return claims, nil } } return nil, err }
在上面的util中,setting包並無定義,因此在這個步驟中咱們須要定義setting包。
使用go get命令安裝依賴。
go get gopkg.in/ini.v1
在項目根目錄下新建setting目錄,並在setting目錄下新建setting.go文件,代碼以下。
package setting import ( "gopkg.in/ini.v1" "log" ) type App struct { JwtSecret string } type Server struct { Ip string Port string } type Database struct { Type string User string Password string Host string Name string TablePrefix string } var AppSetting = &App{} var ServerSetting = &Server{} var DatabaseSetting = &Database{} var config *ini.File func Setup() { var err error config, err = ini.Load("config/app.ini") if err != nil { log.Fatal("Fail to parse 'config/app.ini': %v", err) } mapTo("app", AppSetting) mapTo("server", ServerSetting) mapTo("database", DatabaseSetting) } func mapTo(section string, v interface{}) { err := config.Section(section).MapTo(v) if err != nil { log.Fatalf("Cfg.MapTo RedisSetting err: %v", err) } }
在項目根目錄下新建config目錄,並新建app.ini文件,內容以下。
[app] JwtSecret = 233 [server] Ip : localhost Port : 8000 Url : 127.0.0.1:27017 [database] Type = mysql User = $YOUR_USERNAME Password = $YOUR_PASSWORD Host = 127.0.0.1:3306 Name = golang_test TablePrefix = golang_test_
到此爲止,經過jwt token進行鑑權的邏輯已經所有完成,剩下的就須要實現登陸接口來將token在用戶登陸成功以後返回給用戶。
使用go get命令安裝依賴。
go get github.com/astaxie/beego/validation
在router下新建login.go,代碼以下。
package router import ( "github.com/astaxie/beego/validation" "github.com/detectiveHLH/go-backend-starter/consts" "github.com/detectiveHLH/go-backend-starter/util" "github.com/gin-gonic/gin" "net/http" ) type auth struct { Username string `valid:"Required; MaxSize(50)"` Password string `valid:"Required; MaxSize(50)"` } func Login(c *gin.Context) { appG := util.Gin{C: c} valid := validation.Validation{} username := c.Query("username") password := c.Query("password") a := auth{Username: username, Password: password} ok, _ := valid.Valid(&a) if !ok { appG.Response(http.StatusOK, consts.INVALID_PARAMS, nil) return } authService := authentication.Auth{Username: username, Password: password} isExist, err := authService.Check() if err != nil { appG.Response(http.StatusOK, consts.ERROR_AUTH_CHECK_TOKEN_FAIL, nil) return } if !isExist { appG.Response(http.StatusOK, consts.ERROR_AUTH, nil) return } token, err := util.GenerateToken(username, password) if err != nil { appG.Response(http.StatusOK, consts.ERROR_AUTH_TOKEN, nil) return } appG.Response(http.StatusOK, consts.SUCCESS, map[string]string{ "token": token, }) }
在util包下新增response.go文件,代碼以下。
package util import ( "github.com/detectiveHLH/go-backend-starter/consts" "github.com/gin-gonic/gin" ) type Gin struct { C *gin.Context } func (g *Gin) Response(httpCode, errCode int, data interface{}) { g.C.JSON(httpCode, gin.H{ "code": httpCode, "msg": consts.GetMsg(errCode), "data": data, }) return }
除了返回類,login.go中還有關鍵的鑑權邏輯尚未實現。在根目錄下新建service/authentication目錄,在該目錄下新建auth.go文件,代碼以下。
package authentication import "fmt" type Auth struct { Username string Password string } func (a *Auth) Check() (bool, error) { userName := a.Username passWord := a.Password // todo:實現本身的鑑權邏輯 fmt.Println(userName, passWord) return true, nil }
在此處,須要本身真正的根據業務去實現對用戶調用接口的合法性校驗。例如,能夠根據用戶的用戶名和密碼去數據庫作驗證。
修改router.go中的代碼以下。
package router import ( "github.com/detectiveHLH/go-backend-starter/middleware/jwt" "github.com/gin-gonic/gin" ) func InitRouter() *gin.Engine { router := gin.New() router.GET("/login", Login) apiVersionOne := router.Group("/api/v1/") apiVersionOne.Use(jwt.Jwt()) apiVersionOne.GET("hello", func(c *gin.Context) { c.JSON(200, gin.H{ "success": true, "code": 200, "message": "This works", "data": nil, }) }) return router }
能夠看到,咱們在路由文件中加入了/login接口,並使用了咱們自定義的jwt鑑權的中間件。只要是在v1下的路由,請求以前都會先進入jwt中進行鑑權,鑑權經過以後才能繼續往下執行。
到此,咱們使用go run main.go
啓動服務器,訪問http://localhost:8080/api/v1/hello會遇到以下錯誤。
{ "code": 400, "data": null, "msg": "請求參數錯誤" }
這是由於咱們加入了鑑權,凡是須要鑑權的接口,都須要帶上參數token。而要獲取token則必需要先要登陸,假設咱們的用戶名是Tom,密碼是123。以此來調用登陸接口。
http://localhost:8080/login?username=Tom&password=123
在瀏覽器中訪問如上的url以後,能夠看到返回以下。
{ "code": 200, "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRvbSIsInBhc3N3b3JkIjoiMTIzIiwiZXhwIjoxNTYwMTM5MTE3LCJpc3MiOiJnby1iYWNrZW5kLXN0YXJ0ZXIifQ.I-RSi-xVV1Tk_2iBWolF1u94Y7oVBQXnHh6OI2YKJ6U" }, "msg": "ok" }
有了token以後,咱們再調用hello接口,能夠看到數據正常的返回了。
http://localhost:8080/api/v1/hello?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRvbSIsInBhc3N3b3JkIjoiMTIzIiwiZXhwIjoxNTYwMTM5MTE3LCJpc3MiOiJnby1iYWNrZW5kLXN0YXJ0ZXIifQ.I-RSi-xVV1Tk_2iBWolF1u94Y7oVBQXnHh6OI2YKJ6U
通常的處理方法是,前端拿到這個token,利用持久化存儲存下來,而後以後的每次請求都將token寫在header中發給後端。後端先經過header中的token來校驗調用接口的合法性,驗證經過以後才進行真正的接口調用。
而在這我將token寫在了request param中,只是爲了作一個例子來展現。
完成了基本的框架以後,咱們就開始爲接口引入swagger文檔。寫過java的同窗應該對swagger不陌生。往常寫API文檔,都是手寫。即每一個接口的每個參數,都須要手打。
而swagger不同,swagger只須要你在接口上打上幾個註解(Java中的操做),就能夠自動爲你生成swagger文檔。而在go中,咱們是經過註釋的方式來實現的,接下來咱們安裝gin-swagger。
go get github.com/swaggo/gin-swagger go get -u github.com/swaggo/gin-swagger/swaggerFiles go get -u github.com/swaggo/swag/cmd/swag go get github.com/ugorji/go/codec go get github.com/alecthomas/template
引入依賴以後,咱們須要在router/router.go中注入swagger。在import中加入_ "github.com/detectiveHLH/go-backend-starter/docs"
。
並在router := gin.New()
以後加入以下代碼。
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
在router/login.go中的Login函數上方加上以下注釋。
// @Summary 登陸 // @Produce json // @Param username query string true "username" // @Param password query string true "password" // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}" // @Router /login [get]
在項目根目錄下使用swag init
命令來初始化swagger文檔。該命令將會在項目根目錄生成docs目,內容以下。
. ├── docs.go ├── swagger.json └── swagger.yaml
運行main.go,而後在瀏覽器訪問http://localhost:8080/swagger/index.html就能夠看到swagger根據註釋自動生成的API文檔了。
go get github.com/fvbock/endless
package main import ( "fmt" "github.com/detectiveHLH/go-backend-starter/router" "github.com/fvbock/endless" "log" "syscall" ) func main() { r := router.InitRouter() address := fmt.Sprintf("%s:%s", setting.ServerSetting.Ip, setting.ServerSetting.Port) server := endless.NewServer(address, r) server.BeforeBegin = func(add string) { log.Printf("Actual pid is %d", syscall.Getpid()) } err := server.ListenAndServe() if err != nil { log.Printf("Server err: %v", err) } }
對比起沒有go module的依賴管理,如今的go module更像是Node.js中的package.json,也像是Java中的pom.xml,惟一不一樣的是pom.xml須要手動更新。
當咱們拿到有go module項目的時候,不用擔憂下來依賴時,由於版本問題可能致使的一些兼容問題。直接使用go mod中的命令就能夠將制定了版本的依賴所有安裝,其效果相似於Node.js中的npm install
。
go module定位module的方式,與Node.js尋找依賴的邏輯同樣,Node會從當前命令執行的目錄開始,依次向上查找node_modules中是否有這個依賴,直到找到。go則是依次向上查找go.mod文件,來定位一個模塊。
相信以後go以後的依賴管理,會愈來愈好。
Happy hacking.
參考:
往期文章:
相關:
- 我的網站: Lunhao Hu
- 微信公衆號: SH的全棧筆記(或直接在添加公衆號界面搜索微信號LunhaoHu)