本文介紹如何從零開始,使用 Go 語言的一些有代表性的框架萝嘁,實現(xiàn)一個微服務(wù)架構(gòu)的 Restful Web Service。
由于本人沒有 Mac 電腦(因為窮),所以本文的所有環(huán)境配置和測試靡砌、運行都在 Windows 10 環(huán)境下。
本文涉及的主要框架及環(huán)境:
- go珊楼。go語言庫
- JetBrians GoLand通殃。IDE
- gin。Go 的一款常用 Web 框架厕宗,類似于 SpringMVC
- go-micro画舌。Go 的一款微服務(wù)框架堕担,類似于 SpringCloud/Dubbo
- gorm。Go 的一款 ORM 框架曲聂,從設(shè)計風(fēng)格上有點類似于 Hibernate/MyBatis 的混合霹购。
- go-protobuf。protobuf 的 go 語言版本
- consul朋腋。一個類似 zookeeper 的服務(wù)治理軟件齐疙,可以參看這邊文章。這里不總結(jié)原理和環(huán)境配置乍丈,只作為工具使用剂碴。
Go 的初體驗
Go 的環(huán)境配置
去 Go 官網(wǎng) 下載最新版本的 Go 編譯版本安裝包,然后運行轻专,源碼安裝請見其它博客忆矛。
安裝程序會自動設(shè)置部分但非全部環(huán)境變量,所以為了保險起見请垛,檢查并設(shè)置以下所有環(huán)境變量催训。
- GOROOT
安裝時候的根路徑。 - GOBIN
%GOROOT%\bin
//這個環(huán)境變量是安裝時候自己設(shè)置的宗收,但好像不設(shè)置也問題不大漫拭。 - Path
在 Path 里添加
%GOROOT%\bin - GOPATH
這個是 Go 的工作空間,可以參看這篇文章混稽。個人理解采驻,Go 相當(dāng)于集成了 Maven 和 JDK,依賴管理匈勋、編譯等等都無需額外的軟件了礼旅。
變量值是個合法的文件夾的路徑就行,比如 C:\Users\Cachhe\go洽洁。
GoLand 設(shè)置
- 配置代理痘系。針對公司的內(nèi)網(wǎng)環(huán)境,在 File->Settings->Apearance & Behavior->System Settings->HTTP Proxy饿自,配置 Manual proxy configuration汰翠,具體看公司給的代理配置。
- 安裝 Protobuf 的插件昭雌,為了好看复唤。在 File->Settings->Plugins里,搜索 Protobuf Support城豁,安裝完后要重啟苟穆。
測試
新建一個工程,在根目錄下新建一個 test.go,操作方式與 IDEA 差不多雳旅,這里不介紹具體的操作了跟磨。
默認(rèn)創(chuàng)建出來的 go 文件,package 名與文件夾名一致攒盈,如果我們的工程名叫 xg-temp抵拘,GoLand 初始化了一個 xg-temp 文件夾,那么 test.go 的 package 名也就是 xg-temp型豁。為了讓它可執(zhí)行僵蛛,需要改為 main。這是 go 的約定迎变。
如上圖充尉,執(zhí)行,即可在控制臺看見輸出衣形。
項目背景
假設(shè)我們有一款云產(chǎn)品驼侠,我們希望提供 CRUD 功能。
整體設(shè)計
整體上采用微服務(wù)架構(gòu)谆吴,一般來說倒源,一個工程由前端、api模塊(其實也是一個服務(wù))句狼、多個服務(wù)組成笋熬。這里我們只關(guān)注后臺的邏輯,同時為了簡單腻菇,也只包括一個服務(wù)胳螟。
單個模塊采用 MVC 的結(jié)構(gòu)。從上往下開發(fā)筹吐,先確定與前端的交互協(xié)議旺隙,序列化和反序列化用 protobuf 來做,所以又要定義 proto骏令。WEB控制層通過 gin 來做,Service 與業(yè)務(wù)相關(guān)垄提,DAO 層通過 GORM 來做榔袋。
項目結(jié)構(gòu)如圖:
可見,項目 xg-temp 由 api 和 service 兩個模塊構(gòu)成铡俐,分別對應(yīng) api 模塊 和一個服務(wù)凰兑。
- api
api.go 是程序的入口,負(fù)責(zé)啟動 api 模塊审丘。
client api 模塊雖然對前端體現(xiàn)的是“后端”的作用吏够,但是對于下游的服務(wù)仍然體現(xiàn)的是“客戶端”的作用。請求到了 api 后需要調(diào)下游服務(wù)來處理,雖然暴露出去的一般是 Restful 接口锅知,但是系統(tǒng)內(nèi)部一般用更方便的 rpc 來交互播急。
handler handler 里面定義了控制層的邏輯,包括路徑映射售睹、請求轉(zhuǎn)發(fā)和返回
proto proto 里放 proto 文件桩警,里面定義類似于 Java 的 POJO,可以通過程序自動生成 go 文件和 微服務(wù)需要的文件昌妹。這里因為只是 demo 就沒有放捶枢。
apitest.http JetBrains 提供的一個非常方便的 HTTP 測試工具,可以非常方便地寫 HTTP 請求 - service
common 放公共基礎(chǔ)庫飞崖,比如配置文件
handler 具體業(yè)務(wù)邏輯烂叔,包括操作數(shù)據(jù)庫
proto 同上
config.yaml 配置文件,springboot 也是用的這個
service.go service 模塊的程序入口
安裝必要的軟件及依賴包
安裝 consul
consul 的下載地址固歪,Windows 下的 consul 是一個打包好的 .exe蒜鸡,打開 shell,執(zhí)行
consul.exe agent -dev
看見類似下面的輸出昼牛,即代表啟動了
安裝 protobuf
protobuf 的 GitHub releases 里有 Windows 版本的二進制文件术瓮,下載下來后,解壓贰健,將 protobuf 的 bin 目錄添加到 Path 環(huán)境變量里胞四。
安裝 go-micro 及一些工具
# -u 簡單來說就是遞歸下載每個庫所依賴的庫,不加只會下載這個庫伶椿,但是 -u 不會更新已經(jīng)有的庫辜伟,就算發(fā)現(xiàn)新版本
# -v 會在 shell 里輸出下載了哪些,方便看哪出問題了
# consul client
go get -u github.com/hashicorp/consul
# micro 核心庫
go get -u github.com/micro/micro
# 根據(jù) protoc 生成 micro 的代碼
go get -u github.com/micro/protoc-gen-micro
# protobuf 核心庫
go get -u github.com/golang/protobuf/proto
# 一個用 go 寫的 protobuf 生成軟件
go get -u github.com/golang/protobuf/protoc-gen-go
如果遇到下載不下來的情況下需要用代理脊另,一般是因為被墻了导狡。還是下載不了,就得通過 git 直接下載到 GOPATH 里偎痛。見 這篇文章旱捧。
Api 模塊
routers.go
routers 里定義路徑映射,即不同的 URL踩麦、不同的請求方法怎樣處理枚赡。對比 SpringMVC 中的 Controller,非常好理解谓谦。
package handler
import "github.com/gin-gonic/gin"
func NewWebService() *gin.Engine {
router := gin.Default() // 相當(dāng)于一個 http-server
v1 := router.Group("/v1")
// 路徑分組贫橙,也就是所有訪問 /v1/* 都會被這個 group 攔截
app := v1.Group("/app")
// 同上。v1 表示 api 的版本是 1反粥,app 上的操作都被攔截到這個 group 上進行處理
appManager := new(AppManager)
// 定義了一個類卢肃,這樣做完全是出于工程考慮疲迂,對 app 的請求都在這個接收者的方法上進行處理,避免混亂
app.POST("/create", appManager.CreateApp)
// 對 create 路徑的 POST 請求由 appManager 上的 CreateApp 方法處理莫湘,CreateApp 接收 gin.Context 參數(shù)尤蒿,這個參數(shù)中可以獲取當(dāng)前請求的信息和一些其它信息
app.POST("/delete", appManager.DeleteApp)
app.POST("/update", appManager.UpdateApp)
app.POST("/query", appManager.QueryApp)
product := v1.Group("/product")
pdtManager := new(ProductManager)
product.POST("/create", pdtManager.CreateProduct)
product.POST("/delete", pdtManager.DeleteProduct)
product.POST("/update", pdtManager.UpdateProduct)
product.POST("/query", pdtManager.QueryProduct)
return router
}
app_manager.go
app 相關(guān)的控制層邏輯,也可以直接寫在 routers 里逊脯,但是這樣更軟件工程月洛。product_manager.go 也是一樣撕贞,這里就不解釋了。
代碼只實現(xiàn)了一個,其它的類似醋火,不外乎更復(fù)雜的業(yè)務(wù)邏輯瘟栖。
package handler
import (
"github.com/gin-gonic/gin"
"xg-temp/api/client"
appProto "xg-temp/service/proto"
)
type AppManager struct {}
func(a *AppManager) CreateApp(c *gin.Context) {
var mobileAppCreateRequest appProto.MobileApplication // 聲明一個反序列化后的接收對象
if err := c.ShouldBindJSON(mobileAppCreateRequest); err != nil {
// bind 可以很方便地從 URL query params 里或者 requestBody 里反序列化出對象渊啰,具體用法請查看相關(guān)文檔
resp := new(appProto.AppResponse)
resp.ErrMsg = "請求錯誤"
resp.RetCode = 400
c.JSON(400, resp) // 出錯了贯卦,可能是請求參數(shù)不對,返回狀態(tài)碼 400甘桑,body 部分是 JSON 格式的 resp
c.Abort() // 執(zhí)行完當(dāng)前 handler 后就不再執(zhí)行后續(xù)的 handler 了拍皮。
// 這個有點類似于 Java Struts 中的攔截器/Filter,框架允許類似 pipe 一樣注冊多個 handler 來處理請求跑杭,比如日志铆帽、鑒權(quán)
return
}
// 通過 rpc 調(diào)用下游服務(wù)的 CreateAppInfo 并獲取返回值
resp := client.CreateAppInfo(mobileAppCreateRequest)
c.JSON(200, resp)
}
func(a *AppManager) DeleteApp(c *gin.Context) {
c.JSON(200, gin.H{ // gin.H 是個很方便的生成 JSON 的方法
"error": false,
"data": "你好啊 朋友",
})
}
func(a *AppManager) UpdateApp(c *gin.Context) {
c.JSON(200, gin.H{
"error": false,
"data": "你好啊 朋友",
})
}
func(a *AppManager) QueryApp(c *gin.Context) {
c.JSON(200, gin.H{
"error": false,
"data": "你好啊 朋友",
})
}
api.go
api go
package main // main 是 go 中間一個特殊的 package,表示是程序入口德谅,不然的話就算有 main 方法還是不能執(zhí)行
import (
"fmt"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/registry/consul"
"github.com/micro/go-micro/web"
"time"
"xg-temp/api/client"
"xg-temp/api/handler"
)
var (
listenPort = "localhost:8080" // Restful 接口的 IP 和端口
)
func main() {
serviceName := client.API_SERVICE_NAME
serviceVersion := "latest"
fmt.Printf("start service:%s version:%s", serviceName, serviceVersion)
// 連接本地 consul client
// registry.Option 是一個函數(shù)爹橱,這個函數(shù)接受 Options 為入?yún)? Options 是一個 stuct,里面有 Addr, Timeout 等字段窄做,
// 其中 Addr 為 host:port 格式愧驱,是 consul 服務(wù)的監(jiān)聽地址,默認(rèn)的情況下是本地的 8500 端口椭盏,這個可以在前面啟動 consul 服務(wù)的時候設(shè)置组砚、看見
// 這個設(shè)計在 Java/Android 的回調(diào)方法設(shè)計中很常見
opts := registry.Option(
func(opts *registry.Options) {
opts.Addrs = []string {fmt.Sprintf("%s:%d", "127.0.0.1", 8500)}
})
// 注冊 webservice 到 consul
// consul 一般是 service.NewService,web 是一種特別的 service
service := web.NewService(
web.Name(serviceName),
web.Address(listenPort), // web 服務(wù)的地址
web.RegisterTTL(time.Second * 10),
web.RegisterInterval(time.Second * 5),
web.Registry(consul.NewRegistry(opts))) // consul 的 Options掏颊,包括 consul 的地址
_ = service.Init()
// 初始化控制層
gin := handler.NewWebService() // 拿到一個 gin Engine
// 初始化 micro service
client.InitApiService()
// 綁定 gin 到 "/" 路徑上處理所有請求
service.Handle("/", gin)
// 啟動 service
if err := service.Run(); err != nil {
println(err) // 出錯了
}
}
測試
先將前面涉及 client 的代碼注釋掉糟红,那部分是調(diào) rpc 的,我們還沒寫乌叶。然后 run api.go改化,這個時候我們是無法訪問 create 接口的,但是其它接口是可以的枉昏。在 apitest.http 里定義 HTTP 請求,運行
client.go
client.go 暫時沒法寫揍鸟,因為一般是先有 service 接口前端才能調(diào)兄裂,所以我們先切換到 service 模塊句旱,看看怎么繼續(xù)做。
Service 模塊
Service 主要處理兩件事晰奖,暴露給前端 RPC 接口谈撒,執(zhí)行具體的業(yè)務(wù)邏輯。一般情況下匾南,我們需要自己去定義接口啃匿,然后與 RPC 框架綁定起來,然后再實現(xiàn)這些接口蛆楞。proto 提供了更為方便的途徑溯乒,我們只需要在 proto 文件里聲明 Service 和 數(shù)據(jù)結(jié)構(gòu),然后就可以自動生成對應(yīng)的數(shù)據(jù)結(jié)構(gòu)定義(類似 POJO/Entity) 和 微服務(wù)代碼豹爹,我們只需要去實現(xiàn)一個個接口就行了裆悄。
在 Java 中,自動生成類已經(jīng)是很常見的框架功能臂聋,比如 MyBatis-Generator/GreenDao光稼,但 Proto 功能更強大。Java 中的 RPC 一般是通過動態(tài)代理來做的孩等,go 似乎沒這個功能艾君,所以稍微比 Java 的 RPC 框架用起來麻煩一些。
proto
首先來看 app.proto
syntax = "proto3"; // 語法版本
package xgtmp.srv.app; // 生成的 go 文件會是怎樣的 package name
// 定義 RPC 接口
service App {
// 創(chuàng)建APP信息
rpc CreateAppInfo (MobileApplication) returns (AppResponse) {}
// 更新APP信息
rpc UpdateAppInfo (MobileApplication) returns (AppResponse) {}
// 刪除App信息
rpc DeleteAppInfo (MobileApplication) returns (AppResponse) {}
// 查詢APP信息
rpc QueryAppInfo (AccessId) returns (AppResponse) {}
}
// 類似于聲明一個類
message AccessId {
uint32 accessId = 1; // app 的ID肄方,后面的數(shù)字需要唯一冰垄,是在序列化/反序列化時候的順序
}
message MobileApplication {
string appName = 1; // app名稱
string otherInfo = 2; // 其它信息
}
message AppResponse {
string errMsg = 1;
int32 retCode = 2;
MobileApplication app= 3; // 意思是 AppResponse 里面會包含一個 MobileApplication 的值,
// 這里也可以像下面一樣用引用扒秸,雖然序列化/反序列化的時候都是 deep-copy播演,但是其它代碼中的行為會不一樣,比如賦值/讀值
// MobileApplicaion* app = 4;
}
定義完了后伴奥,打開 bash写烤,進入 app.proto 所在目錄,執(zhí)行以下命令拾徙,應(yīng)該會額外生成兩個文件洲炊,app.pb.go 是數(shù)據(jù)結(jié)構(gòu),app.micro.go 是 RPC 接口尼啡。
protoc --micro_out=. --go_out=. app.proto
# --micro_out=. 表示在當(dāng)前目錄下生成 micro 文件
# --go_out=. 表示在當(dāng)前目錄下生成 pb 文件
# 常用的還有個 --proto_path暂衡,當(dāng)當(dāng)前的 proto 文件里 import 了不在當(dāng)前路徑的其它 proto 文件時,就需要額外指明崖瞭,比如
protoc --proto_path=$GOPATH/src:. --micro_out=. --go_out=. greeter.proto
# 表示搜索 $GOPATH/src 目錄及其子目錄狂巢。親測 windows 下這個 / 符號會導(dǎo)致出錯,:. 也會书聚,不清楚為什么唧领。
看一下 app.pb.go 里的部分關(guān)鍵代碼藻雌,其它方法被省略了。
type MobileApplication struct {
AppName string `protobuf:"bytes,1,opt,name=appName,proto3" json:"appName,omitempty"`
OtherInfo string `protobuf:"bytes,2,opt,name=otherInfo,proto3" json:"otherInfo,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
主要是反引號中間的部分斩个。這部分定義了 protobuf 基于字節(jié)做序列化和反序列化時候的元數(shù)據(jù)胯杭,以及 json 時候的元數(shù)據(jù),后面的 GORM 也是在這里定義受啥。這與 Java 的注解很像做个。
再來看一下 app.micro.go 里的部分關(guān)鍵代碼。
// Client API for App service
// 這部分代碼是給 RPC 客戶端的滚局,在我們的項目中居暖,也就是 api 模塊里的 client 里可以調(diào)用的接口。
type AppService interface {
// 創(chuàng)建APP信息
CreateAppInfo(ctx context.Context, in *MobileApplication, opts ...client.CallOption) (*AppResponse, error)
// 更新APP信息
UpdateAppInfo(ctx context.Context, in *MobileApplication, opts ...client.CallOption) (*AppResponse, error)
// 刪除App信息
DeleteAppInfo(ctx context.Context, in *MobileApplication, opts ...client.CallOption) (*AppResponse, error)
// 查詢APP信息
QueryAppInfo(ctx context.Context, in *AccessId, opts ...client.CallOption) (*AppResponse, error)
}
type appService struct {
c client.Client
name string
}
/// 獲取一個 Service 實例
func NewAppService(name string, c client.Client) AppService {
if c == nil {
c = client.NewClient()
}
if len(name) == 0 {
name = "xgtmp.srv.app"
}
return &appService{
c: c,
name: name,
}
}
// Server API for App service
// 這部分是服務(wù)需要實現(xiàn)的接口核畴,客戶端調(diào)用上面的接口膝但,rpc 到了服務(wù)端后,會實際調(diào)用下面的接口谤草,一一對應(yīng)跟束。
type AppHandler interface {
// 創(chuàng)建APP信息
CreateAppInfo(context.Context, *MobileApplication, *AppResponse) error
// 更新APP信息
UpdateAppInfo(context.Context, *MobileApplication, *AppResponse) error
// 刪除App信息
DeleteAppInfo(context.Context, *MobileApplication, *AppResponse) error
// 查詢APP信息
QueryAppInfo(context.Context, *AccessId, *AppResponse) error
}
// 服務(wù)端調(diào)用這個方法把一個 micro server 實例綁定到實現(xiàn)了 AppHandler 接口的 struct 上
func RegisterAppHandler(s server.Server, hdlr AppHandler, opts ...server.HandlerOption) error {
// 首先里面定義了一個私有的內(nèi)部接口 app,App 繼承 app丑孩。
type app interface {
CreateAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error
UpdateAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error
DeleteAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error
QueryAppInfo(ctx context.Context, in *AccessId, out *AppResponse) error
}
type App struct {
app
}
// NewHandler 內(nèi)部做了大量的工作冀宴,通過反射創(chuàng)建了所需的元數(shù)據(jù)
h := &appHandler{hdlr}
return s.Handle(s.NewHandler(&App{h}, opts...))
}
// appHandler 繼承 AppHandler
type appHandler struct {
AppHandler
}
// micro 的框架會調(diào)用 appHandler 的 這些方法,這些方法內(nèi)部本身并沒有定義什么內(nèi)容温学,
// 而是直接去調(diào) AppHandelr 里的對應(yīng)實現(xiàn)略贮,而這些實現(xiàn)在上面的 RegisterAppHandler 里由傳入的參數(shù)指定。
func (h *appHandler) CreateAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error {
return h.AppHandler.CreateAppInfo(ctx, in, out)
}
func (h *appHandler) UpdateAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error {
return h.AppHandler.UpdateAppInfo(ctx, in, out)
}
func (h *appHandler) DeleteAppInfo(ctx context.Context, in *MobileApplication, out *AppResponse) error {
return h.AppHandler.DeleteAppInfo(ctx, in, out)
}
func (h *appHandler) QueryAppInfo(ctx context.Context, in *AccessId, out *AppResponse) error {
return h.AppHandler.QueryAppInfo(ctx, in, out)
}
handler 模塊
看完了 proto 的生成文件可能還是一頭霧水仗岖,不知道接下來怎么做逃延,不要慌。
客戶端需要調(diào)用 RPC 服務(wù)轧拄,首先得聲明要調(diào)用哪個服務(wù)揽祥,通過服務(wù)治理軟件,也就是 consul 拿到具體得 ip:port檩电,然后聲明調(diào)用哪個接口拄丰,最后拿到返回結(jié)果。
在 api 模塊里定義 rpc_client.go
package client
import (
"context"
"fmt"
goMicro "github.com/micro/go-micro"
appProto "xg-temp/service/proto"
)
var apiService goMicro.Service // 一個全局變量保存初始化后的實例
var API_SERVICE_NAME = "xgtmp.srv.api"
var APP_SERVICE_NAME = "xgtmp.srv.app"
// 類似于靜態(tài)函數(shù)俐末,返回一個實例值料按。這里的單例與否需要上層來保證
// 這里是創(chuàng)建 api 服務(wù),也就是 api web service卓箫。
func InitApiService() {
apiService = goMicro.NewService(
goMicro.Name(API_SERVICE_NAME),
goMicro.Version("latest"))
apiService.Init()
}
// 將本地調(diào)用载矿,轉(zhuǎn)換成 rpc 調(diào)用。如果數(shù)據(jù)結(jié)構(gòu)不一致烹卒,這里會需要做數(shù)據(jù)結(jié)構(gòu)的轉(zhuǎn)換恢准,包括傳給 rpc 調(diào)用的參數(shù)魂挂,以及收到的返回結(jié)果。
func CreateAppInfo(mobileAppCreateRequest appProto.MobileApplication) * appProto.AppResponse {
// 聲明要找哪個 RPC 服務(wù)馁筐,以及綁定到哪個 client 數(shù)據(jù)結(jié)構(gòu)上
// apiService 做發(fā)起 rpc 請求的一方,所以是 client
appService := appProto.NewAppService(APP_SERVICE_NAME, apiService.Client())
fmt.Printf("Create RPC client for : %v", mobileAppCreateRequest)
// 實際調(diào)用 rpc
resp, err := appService.CreateAppInfo(context.TODO(), &mobileAppCreateRequest)
if err != nil {
println(err.Error())
}
return resp
}
服務(wù)端的 interface 已經(jīng)定義好了坠非,需要實現(xiàn)成具體的業(yè)務(wù)邏輯敏沉。go 里面的 OOP 很抽象,完全不像 Java 那樣清晰明顯炎码。實現(xiàn)一個接口盟迟,只需要包含所有函數(shù)且函數(shù)的名稱、入?yún)⒑统鰠⑼耆恢铝氏校退銓崿F(xiàn)了這個接口了攒菠。
定義一個 dao.go 文件,起名起的不好歉闰,因為這里應(yīng)該不只是 db 上的操作的辖众,更應(yīng)該是 service 層的東西。
type AppDao struct {
DB *gogorm.DB // 數(shù)據(jù)庫鏡像實例和敬,類似于 SessionFactory
}
func (d *AppDao) CreateAppInfo(ctx context.Context, in *proto.MobileApplication, out *proto.AppResponse) error {
fmt.Println("This is Cachhe and we are notified")
// 模擬業(yè)務(wù)處理
in.OtherInfo = "設(shè)置其它屬性"
err := d.DB.Create(in).Error // 插入一條數(shù)據(jù)
if err != nil {
out.ErrMsg = err.Error()
out.RetCode = 500
println(err)
} else {
out.ErrMsg = "沒有錯誤啦"
out.RetCode = 200
out.App = in
}
// additional methods
return nil
}
func (AppDao) UpdateAppInfo(context.Context, *proto.MobileApplication, *proto.AppResponse) error {
panic("implement me")
}
func (AppDao) DeleteAppInfo(context.Context, *proto.MobileApplication, *proto.AppResponse) error {
panic("implement me")
}
func (AppDao) QueryAppInfo(context.Context, *proto.AccessId, *proto.AppResponse) error {
panic("implement me")
}
d.DB.Create(in) 能執(zhí)行成功凹炸,必須要先有數(shù)據(jù)庫、表昼弟、和數(shù)據(jù)庫連接啤它。在 Java 里,我們需要配置數(shù)據(jù)源舱痘、連接池等等变骡,go 里面也是一樣。
在 service 的根目錄創(chuàng)建一個 config.yml 文件
mysql:
hostname: ""
port: 3306
user: "root"
password: "123456"
database: "xg"
在 common 目錄里創(chuàng)建 config.go
package common
import (
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"sync"
)
var m *AppConfig
var once sync.Once // go 的并發(fā)包 sync 里的工具類芭逝,保證某個操作只能執(zhí)行一次
type AppConfig struct {
MySql struct{
User string `yaml:"user"` // yaml 的元數(shù)據(jù)塌碌,定義了怎么解析 yaml 文件
Host string `yaml:"hostname"`
Port string `yaml:"port"`
Password string `yaml:"password"`
DBName string `yaml:"database"`
}
}
// 單例模式
func CfgInstance() *AppConfig {
once.Do(func() {
m,_ = loadConf()
})
return m
}
func loadConf() (*AppConfig, error) {
localConfPath := "C:\\Users\\CocoAdapter\\go\\src\\xg-temp\\service\\config.yaml"
conf := &AppConfig{}
yamlFile, err := ioutil.ReadFile(localConfPath)
if err != nil {
println("Error! Yaml file IOException: %s", err.Error())
os.Exit(1)
}
// 從字節(jié)數(shù)組里反序列化成一個 conf 實例
if err := yaml.Unmarshal(yamlFile, conf); err != nil {
println("Error! Yaml unmarshal exception: %s", err.Error())
os.Exit(1)
}
return conf, nil
}
在 handler 目錄里創(chuàng)建 database.go,定義數(shù)據(jù)庫連接
package handler
import (
_ "github.com/go-sql-driver/mysql" // 這個意思是要執(zhí)行 mysql 該包下的文件里所有init()函數(shù)铝耻,但不會打包這個包誊爹,所以無法通過包名來調(diào)用包中的其他函數(shù)
"github.com/jinzhu/gorm"
"xg-temp/service/common"
)
func CreateMySqlConnection() (*gorm.DB, error) {
host := common.CfgInstance().MySql.Host
port := common.CfgInstance().MySql.Port
if host == "" || host == "localhost" || host == "127.0.0.1"{
host = ""
} else {
// 遠(yuǎn)程 ip
host = "tcp(" + host + ":" + port + ")"
}
user := common.CfgInstance().MySql.User
password := common.CfgInstance().MySql.Password
dbName := common.CfgInstance().MySql.DBName
return gorm.Open("mysql",
user + ":" + password + "@" + host +"/" + dbName + "?charset=utf8&parseTime=true&loc=Local")
}
service.go
現(xiàn)在需要啟動這個 service 模塊。
package main
import (
"fmt"
"github.com/micro/go-micro"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/registry/consul"
"os"
"time"
"xg-temp/service/handler"
proto "xg-temp/service/proto"
)
func main() {
serviceName := "xgtmp.srv.app"
db ,err := handler.CreateMySqlConnection()
defer db.Close()
if err != nil {
println("conncet error: &s", err.Error())
os.Exit(1)
}
appDao := &handler.AppDao{DB:db} // 將數(shù)據(jù)源傳給 service 對象
// 這里是通過 gorm 的 DDL 功能瓢捉,直接創(chuàng)建表频丘。但是仍需要先手動建庫。
// 假設(shè)要么所有表都存在要么都不存在
if !appDao.DB.HasTable(&proto.MobileApplication{}) {
if err := appDao.DB.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8",
// gorm 能直接根據(jù) struct 生成對應(yīng)的表泡态,具體請見 gorm 文檔
).CreateTable(&proto.MobileApplication{}, &proto.ProductInfo{}, &proto.Product2App{},
).Error; err != nil {
println("Error: Create table error")
os.Exit(1)
}
}
// 連接本地 consul
opts := registry.Option(func(opts *registry.Options) {
opts.Addrs = []string {fmt.Sprintf("%s:%d", "127.0.0.1", 8500)}
})
// New Service
service := micro.NewService(
micro.Name(serviceName),
micro.Version("latest"),
micro.RegisterTTL(time.Second * 10),
micro.RegisterInterval(time.Second * 5),
micro.Registry(consul.NewRegistry(opts)),
)
// Init & Binding handling
service.Init()
_ = proto.RegisterAppHandler(service.Server(), appDao)
// Run Service
if err := service.Run(); err != nil {
println(err)
}
}
一起 run 起來
api 和 service 是兩個模塊搂漠,都需要運行起來。在運行之前某弦,先要運行 consul桐汤,這樣服務(wù)才能注冊上去而克。啟動順序不重要,因為 api 模塊只有在被訪問的時候才會去調(diào) service 模塊怔毛。
在 http 文件里定義一個新的 POST 請求
查看 api 模塊的控制臺輸出
查看 service 模塊的控制臺輸出
查看數(shù)據(jù)庫
總結(jié)
本文介紹了怎樣使用 go 下面的一些框架和軟件來搭建一個微服務(wù)架構(gòu)的 Restful Service员萍。但是,各方面都沒有特別深入拣度,比如碎绎,沒有探索 consul 的原理和分布式生產(chǎn)環(huán)境下的配置方法,沒有探索 go-gin抗果、go-micro筋帖、gorm等的內(nèi)部原理,及其高級用法冤馏。