成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Golang 微服務(wù)教程(三)

Drummor / 2071人閱讀

摘要:本節(jié)將學(xué)習(xí)來(lái)統(tǒng)一管理和部署微服務(wù),引入第三個(gè)微服務(wù)并進(jìn)行存儲(chǔ)數(shù)據(jù)。到目前為止,要想啟動(dòng)微服務(wù)的容器,均在其中的同時(shí)設(shè)置其環(huán)境變量,服務(wù)多了以后管理起來(lái)十分麻煩。

譯文鏈接:wuYin/blog
原文鏈接:ewanvalentine.io,翻譯已獲作者 Ewan Valentine 授權(quán)。

本文完整代碼:GitHub

在上節(jié)中,我們使用 go-micro 重新實(shí)現(xiàn)了微服務(wù)并進(jìn)行了 Docker 化,但是每個(gè)微服務(wù)都要多帶帶維護(hù)自己的 Makefile 未免過(guò)于繁瑣。本節(jié)將學(xué)習(xí) docker-compose 來(lái)統(tǒng)一管理和部署微服務(wù),引入第三個(gè)微服務(wù) user-service 并進(jìn)行存儲(chǔ)數(shù)據(jù)。

MongoDB 與 Postgres 微服務(wù)的數(shù)據(jù)存儲(chǔ)

到目前為止,consignment-cli 要托運(yùn)的貨物數(shù)據(jù)直接存儲(chǔ)在 consignment-service 管理的內(nèi)存中,當(dāng)服務(wù)重啟時(shí)這些數(shù)據(jù)將會(huì)丟失。為了便于管理和搜索貨物信息,需將其存儲(chǔ)到數(shù)據(jù)庫(kù)中。

可以為每個(gè)獨(dú)立運(yùn)行的微服務(wù)提供獨(dú)立的數(shù)據(jù)庫(kù),不過(guò)因?yàn)楣芾矸爆嵣儆腥诉@么做。如何為不同的微服務(wù)選擇合適的數(shù)據(jù)庫(kù),可參考:How to choose a database for your microservices

選擇關(guān)系型數(shù)據(jù)庫(kù)與 NoSQL

如果對(duì)存儲(chǔ)數(shù)據(jù)的可靠性、一致性要求不那么高,那 NoSQL 將是很好的選擇,因?yàn)樗艽鎯?chǔ)的數(shù)據(jù)格式十分靈活,比如常常將數(shù)據(jù)存為 JSON 進(jìn)行處理,在本節(jié)中選用性能和生態(tài)俱佳的MongoDB

如果要存儲(chǔ)的數(shù)據(jù)本身就比較完整,數(shù)據(jù)之間關(guān)系也有較強(qiáng)關(guān)聯(lián)性的話,可以選用關(guān)系型數(shù)據(jù)庫(kù)。事先捋一下要存儲(chǔ)數(shù)據(jù)的結(jié)構(gòu),根據(jù)業(yè)務(wù)看一下是讀更多還是寫更多?高頻查詢的復(fù)不復(fù)雜?… 鑒于本文的較小的數(shù)據(jù)量與操作,作者選用了 Postgres,讀者可自行更換為 MySQL 等。

更多參考:如何選擇NoSQL數(shù)據(jù)庫(kù)、梳理關(guān)系型數(shù)據(jù)庫(kù)和NoSQL的使用情景

docker-compose 引入原因

上節(jié)把微服務(wù) Docker 化后,使其運(yùn)行在輕量級(jí)、只包含服務(wù)必需依賴的容器中。到目前為止,要想啟動(dòng)微服務(wù)的容器,均在其 Makefile 中 docker run 的同時(shí)設(shè)置其環(huán)境變量,服務(wù)多了以后管理起來(lái)十分麻煩。

基本使用

docker-compose 工具能直接用一個(gè) docker-compose.yaml 來(lái)編排管理多個(gè)容器,同時(shí)設(shè)置各容器的 metadata 和 run-time 環(huán)境(環(huán)境變量),文件的 service 配置項(xiàng)來(lái)像先前 docker run 命令一樣來(lái)啟動(dòng)容器。舉個(gè)例子:

docker 命令管理容器

$ docker run -p 50052:50051 
  -e MICRO_SERVER_ADDRESS=:50051 
  -e MICRO_REGISTRY=mdns 
  vessel-service

等效于 docker-compose 來(lái)管理

version: "3.1"
vessel-service:
  build: ./vessel-service
  ports:
    - 50052:50051
  environment:
    MICRO_ADRESS: ":50051"
    MICRO_REGISTRY: "mdns"

想加減和配置微服務(wù),直接修改 docker-compose.yaml,是十分方便的。

更多參考:使用 docker-compose 編排容器

編排當(dāng)前項(xiàng)目的容器

針對(duì)當(dāng)前項(xiàng)目,使用 docker-compose 管理 3 個(gè)容器,在項(xiàng)目根目錄下新建文件:

# docker-compose.yaml
# 同樣遵循嚴(yán)格的縮進(jìn)
version: "3.1"

# services 定義容器列表
services:
   consignment-cli:
    build: ./consignment-cli
    environment:
      MICRO_REGISTRY: "mdns"

  consignment-service:
    build: ./consignment-service
    ports:
      - 50051:50051
    environment:
      MICRO_ADRESS: ":50051"
      MICRO_REGISTRY: "mdns"
      DB_HOST: "datastore:27017"

  vessel-service:
    build: ./vessel-service
    ports:
      - 50052:50051
    environment:
      MICRO_ADRESS: ":50051"
      MICRO_REGISTRY: "mdns"

首先,我們指定了要使用的 docker-compose 的版本是 3.1,然后使用 services 來(lái)列出了三個(gè)待管理的容器。

每個(gè)微服務(wù)都定義了自己容器的名字, build 指定目錄下的 Dockerfile 將會(huì)用來(lái)編譯鏡像,也可以直接使用 image 選項(xiàng)直接指向已編譯好的鏡像(后邊會(huì)用到);其他選項(xiàng)則指定了容器的端口映射規(guī)則、環(huán)境變量等。

可使用 docker-compose build 來(lái)編譯生成三個(gè)對(duì)應(yīng)的鏡像;使用 docker-compose run 來(lái)運(yùn)行指定的容器, docker-compose up -d 可在后臺(tái)運(yùn)行;使用 docker stop $(docker ps -aq ) 來(lái)停止所有正在運(yùn)行的容器。

運(yùn)行效果

使用 docker-compose 的運(yùn)行效果如下:

Protobuf 與數(shù)據(jù)庫(kù)操作 復(fù)用及其局限性

到目前為止,我們的兩個(gè) protobuf 協(xié)議文件,定義了微服務(wù)客戶端與服務(wù)端數(shù)據(jù)請(qǐng)求、響應(yīng)的數(shù)據(jù)結(jié)構(gòu)。由于 protobuf 的規(guī)范性,也可將其生成的 struct 作為數(shù)據(jù)庫(kù)表 Model 進(jìn)行數(shù)據(jù)操作。這種復(fù)用有其局限性,比如 protobuf 中數(shù)據(jù)類型必須與數(shù)據(jù)庫(kù)表字段嚴(yán)格一致,二者是高耦合的。很多人并不贊將 protobuf 數(shù)據(jù)結(jié)構(gòu)作為數(shù)據(jù)庫(kù)中的表結(jié)構(gòu):Do you use Protobufs in place of structs ?

中間層邏輯轉(zhuǎn)換

一般來(lái)說(shuō),在表結(jié)構(gòu)變化后與 protobuf 不一致,需要在二者之間做一層邏輯轉(zhuǎn)換,處理差異字段:

func (service *Service) (ctx context.Context, req *proto.User, res *proto.Response) error {
  entity := &models.User{
    Name: req.Name.
    Email: req.Email,
    Password: req.Password, 
  }
  err := service.repo.Create(entity)
    
  // 無(wú)中間轉(zhuǎn)換層
  // err := service.repo.Create(req)
  ... 
}

這樣隔離數(shù)據(jù)庫(kù)實(shí)體 models 和 proto.* 結(jié)構(gòu)體,似乎很方便。但當(dāng) .proto 中定義 message 各種嵌套時(shí),models 也要對(duì)應(yīng)嵌套,比較麻煩。

上邊隔不隔離由讀者自行決定,就我個(gè)人而言,中間用 models 做轉(zhuǎn)換是不太有必要的,protobuf 已足夠規(guī)范,直接使用即可。

consignment-service 重構(gòu)

回頭看第一個(gè)微服務(wù) consignment-service,會(huì)發(fā)現(xiàn)服務(wù)端實(shí)現(xiàn)、接口實(shí)現(xiàn)等都往 main.go 里邊塞,功能跑通了,現(xiàn)在要拆分代碼,使項(xiàng)目結(jié)構(gòu)更加清晰,更易維護(hù)。

MVC 代碼結(jié)構(gòu)

對(duì)于熟悉 MVC 開(kāi)發(fā)模式的同學(xué)來(lái)說(shuō),可能會(huì)把代碼按功能拆分到不同目錄中,比如:

main.go
models/
  user.go
handlers/
  auth.go 
  user.go
services/
  auth.go 
微服務(wù)代碼結(jié)構(gòu)

不過(guò)這種組織方式并不是 Golang 的 style,因?yàn)槲⒎?wù)是切割出來(lái)獨(dú)立的,要做到簡(jiǎn)潔明了。對(duì)于大型 Golang 項(xiàng)目,應(yīng)該如下組織:

main.go
users/
  services/
    auth.go
  handlers/
    auth.go
    user.go
  users/
    user.go
containers/
  services/
    manage.go
  models/
    container.go

這種組織方式叫類別(domain)驅(qū)動(dòng),而不是 MVC 的功能驅(qū)動(dòng)。

consignment-service 的重構(gòu)

由于微服務(wù)的簡(jiǎn)潔性,我們會(huì)把該服務(wù)相關(guān)的代碼全放到一個(gè)文件夾下,同時(shí)為每個(gè)文件起一個(gè)合適的名字。

在 consignmet-service/ 下創(chuàng)建三個(gè)文件:handler.go、datastore.go 和 repository.go

consignmet-service/ 
    ├── Dockerfile
    ├── Makefile
    ├── datastore.go    # 創(chuàng)建與 MongoDB 的主會(huì)話
    ├── handler.go        # 實(shí)現(xiàn)微服務(wù)的服務(wù)端,處理業(yè)務(wù)邏輯
    ├── main.go            # 注冊(cè)并啟動(dòng)服務(wù)
    ├── proto
    └── repository.go    # 實(shí)現(xiàn)數(shù)據(jù)庫(kù)的基本 CURD 操作
負(fù)責(zé)連接 MongoDB 的 datastore.go
package main
import "gopkg.in/mgo.v2"

// 創(chuàng)建與 MongoDB 交互的主回話
func CreateSession(host string) (*mgo.Session, error) {
    s, err := mgo.Dial(host)
    if err != nil {
        return nil, err
    }
    s.SetMode(mgo.Monotonic, true)
    return s, nil
}

連接 MongoDB 的代碼夠精簡(jiǎn),傳參是數(shù)據(jù)庫(kù)地址,返回?cái)?shù)據(jù)庫(kù)會(huì)話以及可能發(fā)生的錯(cuò)誤,在微服務(wù)啟動(dòng)的時(shí)候就會(huì)去連接數(shù)據(jù)庫(kù)。

負(fù)責(zé)與 MongoDB 交互的 repository.go

現(xiàn)在讓我們來(lái)將 main.go 與數(shù)據(jù)庫(kù)交互的代碼拆解出來(lái),可以參考注釋加以理解:

package main
import (...)

const (
    DB_NAME        = "shippy"
    CON_COLLECTION = "consignments"
)

type Repository interface {
    Create(*pb.Consignment) error
    GetAll() ([]*pb.Consignment, error)
    Close()
}

type ConsignmentRepository struct {
    session *mgo.Session
}

// 接口實(shí)現(xiàn)
func (repo *ConsignmentRepository) Create(c *pb.Consignment) error {
    return repo.collection().Insert(c)
}

// 獲取全部數(shù)據(jù)
func (repo *ConsignmentRepository) GetAll() ([]*pb.Consignment, error) {
    var cons []*pb.Consignment
    // Find() 一般用來(lái)執(zhí)行查詢,如果想執(zhí)行 select * 則直接傳入 nil 即可
    // 通過(guò) .All() 將查詢結(jié)果綁定到 cons 變量上
    // 對(duì)應(yīng)的 .One() 則只取第一行記錄
    err := repo.collection().Find(nil).All(&cons)
    return cons, err
}

// 關(guān)閉連接
func (repo *ConsignmentRepository) Close() {
    // Close() 會(huì)在每次查詢結(jié)束的時(shí)候關(guān)閉會(huì)話
    // Mgo 會(huì)在啟動(dòng)的時(shí)候生成一個(gè) "主" 會(huì)話
    // 你可以使用 Copy() 直接從主會(huì)話復(fù)制出新會(huì)話來(lái)執(zhí)行,即每個(gè)查詢都會(huì)有自己的數(shù)據(jù)庫(kù)會(huì)話
    // 同時(shí)每個(gè)會(huì)話都有自己連接到數(shù)據(jù)庫(kù)的 socket 及錯(cuò)誤處理,這么做既安全又高效
    // 如果只使用一個(gè)連接到數(shù)據(jù)庫(kù)的主 socket 來(lái)執(zhí)行查詢,那很多請(qǐng)求處理都會(huì)阻塞
    // Mgo 因此能在不使用鎖的情況下完美處理并發(fā)請(qǐng)求
    // 不過(guò)弊端就是,每次查詢結(jié)束之后,必須確保數(shù)據(jù)庫(kù)會(huì)話要手動(dòng) Close
    // 否則將建立過(guò)多無(wú)用的連接,白白浪費(fèi)數(shù)據(jù)庫(kù)資源
    repo.session.Close()
}

// 返回所有貨物信息
func (repo *ConsignmentRepository) collection() *mgo.Collection {
    return repo.session.DB(DB_NAME).C(CON_COLLECTION)
}
拆分后的 main.go
package main
import (...)

const (
    DEFAULT_HOST = "localhost:27017"
)

func main() {

    // 獲取容器設(shè)置的數(shù)據(jù)庫(kù)地址環(huán)境變量的值
    dbHost := os.Getenv("DB_HOST")
    if dbHost == ""{
         dbHost = DEFAULT_HOST
    }
    session, err := CreateSession(dbHost)
    // 創(chuàng)建于 MongoDB 的主會(huì)話,需在退出 main() 時(shí)候手動(dòng)釋放連接
    defer session.Close()
    if err != nil {
        log.Fatalf("create session error: %v
", err)
    }

    server := micro.NewService(
        // 必須和 consignment.proto 中的 package 一致
        micro.Name("go.micro.srv.consignment"),
        micro.Version("latest"),
    )

    // 解析命令行參數(shù)
    server.Init()
    // 作為 vessel-service 的客戶端
    vClient := vesselPb.NewVesselServiceClient("go.micro.srv.vessel", server.Client())
    // 將 server 作為微服務(wù)的服務(wù)端
    pb.RegisterShippingServiceHandler(server.Server(), &handler{session, vClient})

    if err := server.Run(); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
實(shí)現(xiàn)服務(wù)端的 handler.go

將 main.go 中實(shí)現(xiàn)微服務(wù)服務(wù)端 interface 的代碼多帶帶拆解到 handler.go,實(shí)現(xiàn)業(yè)務(wù)邏輯的處理。

package main
import (...)

// 微服務(wù)服務(wù)端 struct handler 必須實(shí)現(xiàn) protobuf 中定義的 rpc 方法
// 實(shí)現(xiàn)方法的傳參等可參考生成的 consignment.pb.go
type handler struct {
    session *mgo.Session
    vesselClient vesselPb.VesselServiceClient
}

// 從主會(huì)話中 Clone() 出新會(huì)話處理查詢
func (h *handler)GetRepo()Repository  {
    return &ConsignmentRepository{h.session.Clone()}
}

func (h *handler)CreateConsignment(ctx context.Context, req *pb.Consignment, resp *pb.Response) error {
    defer h.GetRepo().Close()

    // 檢查是否有適合的貨輪
    vReq := &vesselPb.Specification{
        Capacity:  int32(len(req.Containers)),
        MaxWeight: req.Weight,
    }
    vResp, err := h.vesselClient.FindAvailable(context.Background(), vReq)
    if err != nil {
        return err
    }

    // 貨物被承運(yùn)
    log.Printf("found vessel: %s
", vResp.Vessel.Name)
    req.VesselId = vResp.Vessel.Id
    //consignment, err := h.repo.Create(req)
    err = h.GetRepo().Create(req)
    if err != nil {
        return err
    }
    resp.Created = true
    resp.Consignment = req
    return nil
}

func (h *handler)GetConsignments(ctx context.Context, req *pb.GetRequest, resp *pb.Response) error {
    defer h.GetRepo().Close()
    consignments, err := h.GetRepo().GetAll()
    if err != nil {
        return err
    }
    resp.Consignments = consignments
    return nil
}

至此,main.go 拆分完畢,代碼文件分工明確,十分清爽。

mgo 庫(kù)的 Copy() 與 Clone()

在 handler.go 的 GetRepo() 中我們使用 Clone() 來(lái)創(chuàng)建新的數(shù)據(jù)庫(kù)連接。

可看到在 main.go 中創(chuàng)建主會(huì)話后我們就再也沒(méi)用到它,反而使用 session.Clonse() 來(lái)創(chuàng)建新的會(huì)話進(jìn)行查詢處理,可以看 repository.go 中 Close() 的注釋,如果每次查詢都用主會(huì)話,那所有請(qǐng)求都是同一個(gè)底層 socket 執(zhí)行查詢,后邊的請(qǐng)求將會(huì)阻塞,不能發(fā)揮 Go 天生支持并發(fā)的優(yōu)勢(shì)。

為了避免請(qǐng)求的阻塞,mgo 庫(kù)提供了 Copy()Clone() 函數(shù)來(lái)創(chuàng)建新會(huì)話,二者在功能上相差無(wú)幾,但在細(xì)微之處卻有重要的區(qū)別。Clone 出來(lái)的新會(huì)話重用了主會(huì)話的 socket,避免了創(chuàng)建 socket 在三次握手時(shí)間、資源上的開(kāi)銷,尤其適合那些快速寫入的請(qǐng)求。如果進(jìn)行了復(fù)雜查詢、大數(shù)據(jù)量操作時(shí)依舊會(huì)阻塞 socket 導(dǎo)致后邊的請(qǐng)求阻塞。Copy 為會(huì)話創(chuàng)建新的 socket,開(kāi)銷大。

應(yīng)當(dāng)根據(jù)應(yīng)用場(chǎng)景不同來(lái)選擇二者,本文的查詢既不復(fù)雜數(shù)據(jù)量也不大,就直接復(fù)用主會(huì)話的 socket 即可。不過(guò)用完都要 Close(),謹(jǐn)記。

vessel-service 重構(gòu)

拆解完 consignment-service/main.go 的代碼,現(xiàn)在用同樣的方式重構(gòu) vessel-service

新增貨輪

我們?cè)诖颂砑右粋€(gè)方法:添加新的貨輪,更改 protobuf 文件如下:

syntax = "proto3";
package go.micro.srv.vessel;

service VesselService {
    // 檢查是否有能運(yùn)送貨物的輪船
    rpc FindAvailable (Specification) returns (Response) {}
    // 創(chuàng)建貨輪
    rpc Create(Vessel) returns (Response){}
}

// ...

// 貨輪裝得下的話
// 返回的多條貨輪信息
message Response {
    Vessel vessel = 1;
    repeated Vessel vessels = 2;
    bool created = 3;
}

我們創(chuàng)建了一個(gè) Create() 方法來(lái)創(chuàng)建新的貨輪,參數(shù)是 Vessel 返回 Response,注意 Response 中添加了 created 字段,標(biāo)識(shí)是否創(chuàng)建成功。使用 make build 生成新的 vessel.pb.go 文件。

拆分?jǐn)?shù)據(jù)庫(kù)操作與業(yè)務(wù)邏輯處理

之后在對(duì)應(yīng)的 repository.go 和 handler.go 中實(shí)現(xiàn) Create()

// vesell-service/repository.go
// 完成與數(shù)據(jù)庫(kù)交互的創(chuàng)建動(dòng)作
func (repo *VesselRepository) Create(v *pb.Vessel) error {
    return repo.collection().Insert(v)
}
// vesell-service/handler.go
func (h *handler) GetRepo() Repository {
    return &VesselRepository{h.session.Clone()}
}

// 實(shí)現(xiàn)微服務(wù)的服務(wù)端
func (h *handler) Create(ctx context.Context, req *pb.Vessel, resp *pb.Response) error {
    defer h.GetRepo().Close()
    if err := h.GetRepo().Create(req); err != nil {
        return err
    }
    resp.Vessel = req
    resp.Created = true
    return nil
}
引入 MongoDB

兩個(gè)微服務(wù)均已重構(gòu)完畢,是時(shí)候在容器中引入 MongoDB 了。在 docker-compose.yaml 添加 datastore 選項(xiàng):

services:
  ...
  datastore:
    image: mongo
    ports:
      - 27017:27017

同時(shí)更新兩個(gè)微服務(wù)的環(huán)境變量,增加 DB_HOST: "datastore:27017",在這里我們使用 datastore 做主機(jī)名而不是 localhost,是因?yàn)?docker 有內(nèi)置強(qiáng)大的 DNS 機(jī)制。參考:docker內(nèi)置dnsserver工作機(jī)制

修改完畢后的 docker-compose.yaml:

# docker-compose.yaml
version: "3.1"

services:
  consigment-cli:
    build: ./consignment-cli
    environment:
      MICRO_REGISTRY: "mdns"

  consignment-service:
    build: ./consignment-service
    ports:
      - 50051:50051
    environment:
      MICRO_ADRESS: ":50051"
      MICRO_REGISTRY: "mdns"
      DB_HOST: "datastore:27017"

  vessel-service:
    build: ./vessel-service
    ports:
      - 50052:50051
    environment:
      MICRO_ADRESS: ":50051"
      MICRO_REGISTRY: "mdns"
      DB_HOST: "datastore:27017"

  datastore:
    image: mongo
    ports:
      - 27017:27017

修改完代碼需重新 make build,構(gòu)建鏡像時(shí)需 docker-compose build --no-cache 來(lái)全部重新編譯。

user-service 引入 Postgres

現(xiàn)在來(lái)創(chuàng)建第三個(gè)微服務(wù),在 docker-compose.yaml 中引入 Postgres:

...
  user-service:
    build: ./user-service
    ports:
      - 50053:50051
    environment:
      MICRO_ADDRESS: ":50051"
      MICRO_REGISTRY: "mdns"

  ...
  database:
    image: postgres
    ports:
      - 5432:5432

在項(xiàng)目根目錄下創(chuàng)建 user-service 目錄,并且像前兩個(gè)服務(wù)那樣依次創(chuàng)建下列文件:

handler.go, main.go, repository.go, database.go, Dockerfile, Makefile,
定義 protobuf 文件

創(chuàng)建 proto/user/user.proto 且內(nèi)容如下:

// user-service/user/user.proto
syntax = "proto3";

package go.micro.srv.user;

service UserService {
    rpc Create (User) returns (Response) {}
    rpc Get (User) returns (Response) {}
    rpc GetAll (Request) returns (Response) {}
    rpc Auth (User) returns (Token) {}
    rpc ValidateToken (Token) returns (Token) {}
}

// 用戶信息
message User {
    string id = 1;
    string name = 2;
    string company = 3;
    string email = 4;
    string password = 5;
}

message Request {
}

message Response {
    User user = 1;
    repeated User users = 2;
    repeated Error errors = 3;
}

message Token {
    string token = 1;
    bool valid = 2;
    Error errors = 3;
}

message Error {
    int32 code = 1;
    string description = 2;
}

確保你的 user-service 有像類似前兩個(gè)微服務(wù)的 Makefile,使用 make build 來(lái)生成 gRPC 代碼。

實(shí)現(xiàn)業(yè)務(wù)邏輯處理的 handler.go

在 handler.go 實(shí)現(xiàn)的服務(wù)端代碼中,認(rèn)證模塊將在下一節(jié)使用 JWT 做認(rèn)證。

// user-service/handler.go

package main

import (
    "context"
    pb "shippy/user-service/proto/user"
)

type handler struct {
    repo Repository
}

func (h *handler) Create(ctx context.Context, req *pb.User, resp *pb.Response) error {
    if err := h.repo.Create(req); err != nil {
        return nil
    }
    resp.User = req
    return nil
}

func (h *handler) Get(ctx context.Context, req *pb.User, resp *pb.Response) error {
    u, err := h.repo.Get(req.Id);
    if err != nil {
        return err
    }
    resp.User = u
    return nil
}

func (h *handler) GetAll(ctx context.Context, req *pb.Request, resp *pb.Response) error {
    users, err := h.repo.GetAll()
    if err != nil {
        return err
    }
    resp.Users = users
    return nil
}

func (h *handler) Auth(ctx context.Context, req *pb.User, resp *pb.Token) error {
    _, err := h.repo.GetByEmailAndPassword(req)
    if err != nil {
        return err
    }
    resp.Token = "`x_2nam"
    return nil
}

func (h *handler) ValidateToken(ctx context.Context, req *pb.Token, resp *pb.Token) error {
    return nil
}
實(shí)現(xiàn)數(shù)據(jù)庫(kù)交互的 repository.go
package main

import (
    "github.com/jinzhu/gorm"
    pb "shippy/user-service/proto/user"
)

type Repository interface {
    Get(id string) (*pb.User, error)
    GetAll() ([]*pb.User, error)
    Create(*pb.User) error
    GetByEmailAndPassword(*pb.User) (*pb.User, error)
}

type UserRepository struct {
    db *gorm.DB
}

func (repo *UserRepository) Get(id string) (*pb.User, error) {
    var u *pb.User
    u.Id = id
    if err := repo.db.First(&u).Error; err != nil {
        return nil, err
    }
    return u, nil
}

func (repo *UserRepository) GetAll() ([]*pb.User, error) {
    var users []*pb.User
    if err := repo.db.Find(&users).Error; err != nil {
        return nil, err
    }
    return users, nil
}

func (repo *UserRepository) Create(u *pb.User) error {
    if err := repo.db.Create(&u).Error; err != nil {
        return err
    }
    return nil
}

func (repo *UserRepository) GetByEmailAndPassword(u *pb.User) (*pb.User, error) {
    if err := repo.db.Find(&u).Error; err != nil {
        return nil, err
    }
    return u, nil
}
使用 UUID

我們將 ORM 創(chuàng)建的 UUID 字符串修改為一個(gè)整數(shù),用來(lái)作為表的主鍵或 ID 是比較安全的。MongoDB 使用了類似的技術(shù),但是 Postgres 需要我們使用第三方庫(kù)手動(dòng)來(lái)生成。在 user-service/proto/user 目錄下創(chuàng)建 extension.go 文件:

package go_micro_srv_user

import (
    "github.com/jinzhu/gorm"
    uuid "github.com/satori/go.uuid"
    "github.com/labstack/gommon/log"
)

func (user *User) BeforeCreate(scope *gorm.Scope) error {
    uuid, err := uuid.NewV4()
    if err != nil {
        log.Fatalf("created uuid error: %v
", err)
    }
    return scope.SetColumn("Id", uuid.String())
}

函數(shù) BeforeCreate() 指定了 GORM 庫(kù)使用 uuid 作為 ID 列值。參考:doc.gorm.io/callbacks

GORM

Gorm 是一個(gè)簡(jiǎn)單易用輕量級(jí)的 ORM 框架,支持 ?Postgres, MySQL, Sqlite 等數(shù)據(jù)庫(kù)。

到目前三個(gè)微服務(wù)涉及到的數(shù)據(jù)量小、操作也少,用原生 SQL 完全可以 hold 住,所以是不是要 ORM 取決于你自己。

user-cli

類比 consignment-service 的測(cè)試,現(xiàn)在創(chuàng)建 user-cli 命令行應(yīng)用來(lái)測(cè)試 user-service

在項(xiàng)目根目錄下創(chuàng)建 user-cli 目錄,并創(chuàng)建 cli.go 文件:

package main

import (
    "log"
    "os"

    pb "shippy/user-service/proto/user"
    microclient "github.com/micro/go-micro/client"
    "github.com/micro/go-micro/cmd"
    "golang.org/x/net/context"
    "github.com/micro/cli"
    "github.com/micro/go-micro"
)


func main() {

    cmd.Init()

    // 創(chuàng)建 user-service 微服務(wù)的客戶端
    client := pb.NewUserServiceClient("go.micro.srv.user", microclient.DefaultClient)

    // 設(shè)置命令行參數(shù)
    service := micro.NewService(
        micro.Flags(
            cli.StringFlag{
                Name:  "name",
                Usage: "You full name",
            },
            cli.StringFlag{
                Name:  "email",
                Usage: "Your email",
            },
            cli.StringFlag{
                Name:  "password",
                Usage: "Your password",
            },
            cli.StringFlag{
                Name: "company",
                Usage: "Your company",
            },
        ),
    )

    service.Init(
        micro.Action(func(c *cli.Context) {
            name := c.String("name")
            email := c.String("email")
            password := c.String("password")
            company := c.String("company")

            r, err := client.Create(context.TODO(), &pb.User{
                Name: name,
                Email: email,
                Password: password,
                Company: company,
            })
            if err != nil {
                log.Fatalf("Could not create: %v", err)
            }
            log.Printf("Created: %v", r.User.Id)

            getAll, err := client.GetAll(context.Background(), &pb.Request{})
            if err != nil {
                log.Fatalf("Could not list users: %v", err)
            }
            for _, v := range getAll.Users {
                log.Println(v)
            }

            os.Exit(0)
        }),
    )

    // 啟動(dòng)客戶端
    if err := service.Run(); err != nil {
        log.Println(err)
    }
}
測(cè)試 運(yùn)行成功

在此之前,需要手動(dòng)拉取 Postgres 鏡像并運(yùn)行:

$ docker pull postgres
$ docker run --name postgres -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres
用戶數(shù)據(jù)創(chuàng)建并存儲(chǔ)成功:

總結(jié)

到目前為止,我們創(chuàng)建了三個(gè)微服務(wù):consignment-service、vessel-service 和 user-service,它們均使用 go-micro 實(shí)現(xiàn)并進(jìn)行了 Docker 化,使用 docker-compose 進(jìn)行統(tǒng)一管理。此外,我們還使用 GORM 庫(kù)與 Postgres 數(shù)據(jù)庫(kù)進(jìn)行交互,并將命令行的數(shù)據(jù)存儲(chǔ)進(jìn)去。

上邊的 user-cli 僅是測(cè)試使用,明文保存密碼一點(diǎn)也不安全。在本節(jié)完成基本功能的基礎(chǔ)上,下節(jié)將引入 JWT 做驗(yàn)證。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/27336.html

相關(guān)文章

  • Golang 服務(wù)教程(二)

    摘要:定義微服務(wù)作為客戶端調(diào)用的函數(shù)實(shí)現(xiàn)中的接口,使作為的服務(wù)端檢查是否有適合的貨輪貨物被承運(yùn)解析命令行參數(shù)作為的客戶端增加貨物并運(yùn)行更新中的貨物,塞入三個(gè)集裝箱,重量和容量都變大。 譯文鏈接:wuYin/blog原文鏈接:ewanvalentine.io,翻譯已獲作者 Ewan Valentine 授權(quán)。 本節(jié)未細(xì)致介紹 Docker,更多可參考:《第一本Docker書 修訂版》 前言 在...

    sevi_stuo 評(píng)論0 收藏0
  • AI開(kāi)發(fā)書籍分享

    摘要:編程書籍的整理和收集最近一直在學(xué)習(xí)深度學(xué)習(xí)和機(jī)器學(xué)習(xí)的東西,發(fā)現(xiàn)深入地去學(xué)習(xí)就需要不斷的去提高自己算法和高數(shù)的能力然后也找了很多的書和文章,隨著不斷的學(xué)習(xí),也整理了下自己的學(xué)習(xí)筆記準(zhǔn)備分享出來(lái)給大家后續(xù)的文章和總結(jié)會(huì)繼續(xù)分享,先分享一部分的 編程書籍的整理和收集 最近一直在學(xué)習(xí)deep learning深度學(xué)習(xí)和機(jī)器學(xué)習(xí)的東西,發(fā)現(xiàn)深入地去學(xué)習(xí)就需要不斷的去提高自己算法和高數(shù)的能力然后...

    huayeluoliuhen 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<