Go微服務架構系列--gin框架(上)

飛狐發表於2020-08-25

image
hi,大家好,小弟飛狐。這次帶來的是Golang微服務系列。Deno從零到架構級系列文章裡就提到過微服務。最近一次專案重構中,採用了go-micro微服務架構。又恰逢deno1.0正式版推出,於是乎node業務層也用deno重寫。把Java的業務模組也全部用go重構了。

Go-micro重構Java業務

重構業務的時候,我們用go-micro來做微服務,全面的替代了Java棧。比如:

  • 服務註冊發現用到了etcd
  • 通訊用到了grpc
  • 框架整合了gin

訂單、支付等等都作為單獨的服務。而deno之上都歸前端來處理業務層,這樣職責明確,更利於前後端協作。另外,我們這套將會採用最新的go-micro V3來搭建架構。

image

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的語法特性,飛狐推薦把路由放到控制層中維護

image

路由改造

路由改造之前我們新建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方法在主函式中執行。這樣就達到目的,不用去維護單獨的路由檔案了。不過,大家發現沒?這樣也帶來了一些弊端。比如:

  • 規範性很差
  • 程式碼耦合性高
  • 靈活性不夠、維護起來就很麻煩

image

搭建腳手架

為了解決上述弊端,基於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()
}

好啦,這篇內容就結束了。下面是彩蛋部分,還有激情的小夥伴,鼓勵繼續學。

image

彩蛋: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)
}

好啦,這篇的內容就全部結束啦,後續內容會講中介軟體、錯誤處理等等。

相關文章