hi,大家好,小弟飛狐。這次帶來的是Golang微服務系列。Deno從零到架構級系列文章裡就提到過微服務。最近一次專案重構中,採用了go-micro微服務架構。又恰逢deno1.0正式版推出,於是乎node業務層也用deno重寫。把Java的業務模組也全部用go重構了。
Go-micro重構Java業務
重構業務的時候,我們用go-micro來做微服務,全面的替代了Java棧。比如:
- 服務註冊發現用到了etcd
- 通訊用到了grpc
- 框架整合了gin
訂單、支付等等都作為單獨的服務。而deno之上都歸前端來處理業務層,這樣職責明確,更利於前後端協作。另外,我們這套將會採用最新的go-micro V3來搭建架構。
gin框架初體驗
話不多說,即刻開始。這套微服務系列不是入門教程,需要有go專案經驗。從框架選型開始,到go-micro構建微服務架構。go的框架選型不用糾結。在go的web框架中,飛狐推薦兩個框架:
- echo
- gin
介紹這兩框架的文章太多了,優勢與區別我就不多說了。這兩個框架大家可以任選其一,可以任憑喜好,那飛狐選擇gin框架,並將gin框架整合到go-micro中。我們先從gin基礎架構搭建開始。先來個簡單的例子,如下:
package main
// 獲取gin
import "github.com/gin-gonic/gin"
// 主函式
func main() {
// 取r是router的縮寫
r := gin.Default()
// 這裡非常簡單,很像deno、node的路由吧
r.GET("/", func(c \*gin.Context) {
c.JSON(200, gin.H{ "message": "pong", })
})
// 監聽埠8080
r.Run(":8080")
}
這個例子非常簡單,直接copy的gin官方程式碼。加了中文註釋,執行即可,相信有點基礎的童鞋都能看懂。這裡的路由,一般會單獨寫檔案來維護。不過,我在deno架構系列中提到過,拿到專案直接就是幹路由,不要去維護一個單獨的路由檔案。deno系列我們用的是註解路由。雖然go也可以通過反射實現註解路由,但go不是一門物件導向的語言。根據go的語法特性,飛狐推薦把路由放到控制層中維護。
路由改造
路由改造之前我們新建controller層,然後操作如下:
// 新建userController.go
package controller
import (
"github.com/gin-gonic/gin"
)
type UserController struct {
*gin.Engine
}
// 這裡是建構函式
func NewUserController(e *gin.Engine) *UserController {
return &UserController{e}
}
// 這裡是業務方法
func (this *UserController) GetUser() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"data": "hello world",
})
}
}
// 這裡是處理路由的地兒
func (this *UserController) Router () {
this.Handle("GET", "/", this.GetUser())
}
這樣路由就維護到每個控制器中了,那如何對映呢?我們改造主檔案如下:
func main () {
r := gin.Default()
NewUserController(r).Router()
r.Run(":8080")
}
關鍵程式碼就是將構造器的Router方法在主函式中執行。這樣就達到目的,不用去維護單獨的路由檔案了。不過,大家發現沒?這樣也帶來了一些弊端。比如:
- 規範性很差
- 程式碼耦合性高
- 靈活性不夠、維護起來就很麻煩
搭建腳手架
為了解決上述弊端,基於gin我們搭建一個腳手架。就如同我們基於oak搭建deno的腳手架一樣。同樣換做echo框架也同樣適用。新建server目錄,在此目錄下新建server.go檔案,程式碼如下:
package server
import (
"github.com/gin-gonic/gin"
)
// 這裡是定義一個介面,解決上述弊端的規範性
type IController interface {
// 這個傳參就是腳手架主程
Router(server *Server)
}
// 定義一個腳手架
type Server struct {
*gin.Engine
// 路由分組一會兒會用到
g *gin.RouterGroup
}
// 初始化函式
func Init() *Server {
// 作為Server的構造器
s := &Server{Engine: gin.New()}
// 返回作為鏈式呼叫
return s
}
// 監聽函式,更好的做法是這裡的埠應該放到配置檔案
func (this *Server) Listen() {
this.Run(":8080")
}
// 這裡是路由的關鍵程式碼,這裡會掛載路由
func (this *Server) Route(controllers ...IController) *Server {
// 遍歷所有的控制層,這裡使用介面,就是為了將Router例項化
for _, c := range controllers {
c.Router(this)
}
return this
}
這一步完成了,主函式就減負了,主函式改造如下:
// main.go
package main
import (
. "feihu/controller"
"feihu/server"
)
// 這裡其實之前飛狐講的deno入口檔案改造幾乎一樣
func main () {
// 這裡就是腳手架提供的服務
server.
// 初始化
Init().
// 路由
Route(
NewUserController(),
).
// 監聽埠
Listen()
}
那控制層的程式碼也會相應簡化,之前的控制層程式碼改造如下:
package controller
import (
"github.com/gin-gonic/gin"
"feihu/server"
)
// 這裡的gin引擎直接移到腳手架server裡
type UserController struct {
}
// 這裡是建構函式
func NewUserController() *UserController {
return &UserController{}
}
// 這裡是業務方法
func (this *UserController) GetUser() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"data": "hello world",
})
}
}
// 這裡依然是處理路由的地兒,而由於我們定義了介面規範,就必須實現Router方法
func (this *UserController) Router (server *server.Server) {
server.Handle("GET", "/", this.GetUser())
}
這樣就比較完善了。不過眾所周知,gin支援路由分組。如何實現呢?我們繼續往下。
路由分組
路由分組只需要在server.go里加一個方法就OK了,程式碼如下:
func (this *Server) GroupRouter(group string, controllers ...IController) *Server {
this.g = this.Group(group)
for _, c := range controllers {
c.Router(this)
}
return this
}
使用路由分組時,主函式main.go的程式碼如下:
package main
import (
. "feihu/controller"
"feihu/server"
)
func main () {
server.
Init().
Route(
NewUserController(),
).
// 這裡就是路由分組啦
GroupRouter("v1",
NewOrderController(),
).
Listen()
}
好啦,這篇內容就結束了。下面是彩蛋部分,還有激情的小夥伴,鼓勵繼續學。
彩蛋:Go設計模式之單例模式
今天的內容其實很輕鬆,加餐部分我們來個Go的設計模式好了。幾年前《聽飛狐聊JavaScript設計模式》中有講到單利模式。JS、Java實現單利模式都特別簡單,但Go不太一樣,我們就拿單利模式來玩玩兒。從最簡單的例子開始
package main
import "fmt"
// 定義結構
type Singleton struct {
MobileUrl string
}
// 變數
var instance *Singleton
// 這裡是單例,返回的是單例結構
func GetSingleton() *Singleton {
// 先判斷變數是否存在,如果不存在才建立
if instance == nil {
instance = &Singleton{MobileUrl: "https://www.aizmen.com"}
}
return instance
}
func main () {
x := GetSingleton() // 單獨列印x,可以得到:&{https://www.aizmen.com}
x1 := GetSingleton() // 單獨列印x1,也得到:&{https://www.aizmen.com}
fmt.Println(x == x1)
}
列印結果為:true,說明是同一塊記憶體。這樣就實現了最簡單的單利模式了。
sync.Once單例模式
Go其實提供了一個更簡潔的sync.Once,實現如下:
package main
import (
"fmt"
"sync"
)
type Singleton struct {
MobileUrl string
}
var (
once sync.Once
instance *Singleton
)
func GetSingleton() *Singleton {
once.Do(func() {
instance = &Singleton{MobileUrl: "https://www.aizmen.com"}
})
return instance
}
func main () {
x := GetSingleton()
x1 := GetSingleton()
fmt.Println(x == x1)
}
眾所周知,Go語言的協程很強大,在使用協程時,可以使用sync.Once來控制。
單例模式之加鎖機制
Go還提供了一個基礎物件sync.Mutex,用以實現協程之間的同步邏輯,程式碼實現如下:
package main
import (
"fmt"
"sync"
)
type Singleton struct {
MobileUrl string
}
var (
once sync.Once
instance *Singleton
mutex sync.Mutex
)
func GetSingleton() *Singleton {
mutex.Lock()
defer mutex.Unlock()
if instance == nil {
instance = &Singleton{MobileUrl: "https://www.aizmen.com"}
}
return instance
}
func main () {
x := GetSingleton()
x1 := GetSingleton()
fmt.Println(x == x1)
}
好啦,這篇的內容就全部結束啦,後續內容會講中介軟體、錯誤處理等等。