用 Go 快速開發一個 RESTful API 服務

kevinwan 發表於 2022-05-16
Go

何時使用單體 RESTful 服務

對於很多初創公司來說,業務的早期我們更應該關注於業務價值的交付,而單體服務具有架構簡單,部署簡單,開發成本低等優點,可以幫助我們快速實現產品需求。我們在使用單體服務快速交付業務價值的同時,也需要為業務的發展預留可能性,所以我們一般會在單體服務中清晰的拆分不同的業務模組。

商城單體 RESTful 服務

我們以商城為例來構建單體服務,商城服務一般來說相對複雜,會由多個模組組成,比較重要的模組包括賬號模組、商品模組和訂單模組等,每個模組會有自己獨立的業務邏輯,同時每個模組間也會相互依賴,比如訂單模組和商品模組都會依賴賬號模組,在單體應用中這種依賴關係一般是通過模組間方法呼叫來完成。一般單體服務會共享儲存資源,比如 MySQLRedis 等。

單體服務的整體架構比較簡單,這也是單體服務的優點,客戶請求通過 DNS 解析後通過 Nginx 轉發到商城的後端服務,商城服務部署在 ECS 雲主機上,為了實現更大的吞吐和高可用一般會部署多個副本,這樣一個簡單的平民架構如果優化好的話也是可以承載較高的吞吐的。

用 Go 快速開發一個 RESTful API 服務

商城服務內部多個模組間存在依賴關係,比如請求訂單詳情介面 /order/detail,通過路由轉發到訂單模組,訂單模組會依賴賬號模組和商品模組組成完整的訂單詳情內容返回給使用者,在單體服務中多個模組一般會共享資料庫和快取。

用 Go 快速開發一個 RESTful API 服務

單體服務實現

接下來介紹如何基於 go-zero 來快速實現商城單體服務。使用過 go-zero 的同學都知道,我們提供了一個 API 格式的檔案來描述 Restful API,然後可以通過 goctl 一鍵生成對應的程式碼,我們只需要在 logic 檔案裡填寫對應的業務邏輯即可。商城服務包含多個模組,為了模組間相互獨立,所以不同模組由單獨的 API 定義,但是所有的 API 的定義都是在同一個 service (mall-api) 下。

api 目錄下分別建立 user.api, order.api, product.apimall.api,其中 mall.api 為聚合的 api 檔案,通過 import 匯入,檔案列表如下:

api
|-- mall.api
|-- order.api
|-- product.api
|-- user.api

Mall API 定義

mall.api 的定義如下,其中 syntax = “v1” 表示這是 zero-apiv1 語法

syntax = "v1"

import "user.api"
import "order.api"
import "product.api"

賬號模組 API 定義

  • 檢視使用者詳情
  • 獲取使用者所有訂單
syntax = "v1"

type (
    UserRequest {
        ID int64 `path:"id"`
    }

    UserReply {
        ID      int64   `json:"id"`
        Name    string  `json:"name"`
        Balance float64 `json:"balance"`
    }

    UserOrdersRequest {
        ID int64 `path:"id"`
    }

    UserOrdersReply {
        ID       string `json:"id"`
        State    uint32 `json:"state"`
        CreateAt string `json:"create_at"`
    }
)

service mall-api {
    @handler UserHandler
    get /user/:id (UserRequest) returns (UserReply)

    @handler UserOrdersHandler
    get /user/:id/orders (UserOrdersRequest) returns (UserOrdersReply)
}

訂單模組 API 定義

  • 獲取訂單詳情
  • 生成訂單
syntax = "v1"

type (
    OrderRequest {
        ID string `path:"id"`
    }

    OrderReply {
        ID       string `json:"id"`
        State    uint32 `json:"state"`
        CreateAt string `json:"create_at"`
    }

    OrderCreateRequest {
        ProductID int64 `json:"product_id"`
    }

    OrderCreateReply {
        Code int `json:"code"`
    }
)

service mall-api {
    @handler OrderHandler
    get /order/:id (OrderRequest) returns (OrderReply)

    @handler OrderCreateHandler
    post /order/create (OrderCreateRequest) returns (OrderCreateReply)
}

商品模組 API 定義

  • 檢視商品詳情
syntax = "v1"

type ProductRequest {
    ID int64 `path:"id"`
}

type ProductReply {
    ID    int64   `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
    Count int64   `json:"count"`
}

service mall-api {
    @handler ProductHandler
    get /product/:id (ProductRequest) returns (ProductReply)
}

生成單體服務

已經定義好了 API,接下來用 API 生成服務就會變得非常簡單,我們使用 goctl 生成單體服務程式碼。

$ goctl api go -api api/mall.api -dir .

生成的程式碼結構如下:

.
├── api
│   ├── mall.api
│   ├── order.api
│   ├── product.api
│   └── user.api
├── etc
│   └── mall-api.yaml
├── internal
│   ├── config
│   │   └── config.go
│   ├── handler
│   │   ├── ordercreatehandler.go
│   │   ├── orderhandler.go
│   │   ├── producthandler.go
│   │   ├── routes.go
│   │   ├── userhandler.go
│   │   └── userordershandler.go
│   ├── logic
│   │   ├── ordercreatelogic.go
│   │   ├── orderlogic.go
│   │   ├── productlogic.go
│   │   ├── userlogic.go
│   │   └── userorderslogic.go
│   ├── svc
│   │   └── servicecontext.go
│   └── types
│       └── types.go
└── mall.go

解釋一下生成的程式碼結構:

  • api:存放 API 描述檔案
  • etc:用來定義專案配置,所有的配置項都可以寫在 mall-api.yaml
  • internal/config:服務的配置定義
  • internal/handlerAPI 檔案中定義的路由對應的 handler 的實現
  • internal/logic:用來放每個路由對應的業務邏輯,之所以區分 handlerlogic 是為了讓業務處理部分儘可能減少依賴,把 HTTP requests 和邏輯處理程式碼隔離開,便於後續拆分成 RPC service
  • internal/svc:用來定義業務邏輯處理的依賴,我們可以在 main 函式裡面建立依賴的資源,然後通過 ServiceContext 傳遞給 handlerlogic
  • internal/types:定義了 API 請求和返回資料結構
  • mall.gomain 函式所在檔案,檔名和 API 定義中的 service 同名,去掉了字尾 -api

生成的服務不需要做任何修改就可以執行:

$ go run mall.go
Starting server at 0.0.0.0:8888...
實現業務邏輯

接下來我們來一起實現一下業務邏輯,出於演示目的邏輯會比較簡單,並非真正業務邏輯。

首先,我們先來實現使用者獲取所有訂單的邏輯,因為在使用者模組並沒有訂單相關的資訊,所以我們需要依賴訂單模組查詢使用者的訂單,所以我們在 UserOrdersLogic 中新增對 OrderLogic 依賴

type UserOrdersLogic struct {
    logx.Logger
    ctx        context.Context
    svcCtx     *svc.ServiceContext
    orderLogic *OrderLogic
}

func NewUserOrdersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserOrdersLogic {
    return &UserOrdersLogic{
        Logger:     logx.WithContext(ctx),
        ctx:        ctx,
        svcCtx:     svcCtx,
        orderLogic: NewOrderLogic(ctx, svcCtx),
    }
}

OrderLogic 中實現根據 使用者id 查詢所有訂單的方法

func (l *OrderLogic) ordersByUser(uid int64) ([]*types.OrderReply, error) {
    if uid == 123 {
        // It should actually be queried from database or cache
        return []*types.OrderReply{
            {
                ID:       "236802838635",
                State:    1,
                CreateAt: "2022-5-12 22:59:59",
            },
            {
                ID:       "236802838636",
                State:    1,
                CreateAt: "2022-5-10 20:59:59",
            },
        }, nil
    }

    return nil, nil
}

UserOrdersLogicUserOrders 方法中呼叫 ordersByUser 方法

func (l *UserOrdersLogic) UserOrders(req *types.UserOrdersRequest) (*types.UserOrdersReply, error) {
    orders, err := l.orderLogic.ordersByUser(req.ID)
    if err != nil {
        return nil, err
    }

    return &types.UserOrdersReply{
        Orders: orders,
    }, nil
}

這時候我們重新啟動 mall-api 服務,在瀏覽器中請求獲取使用者所有訂單介面

http://localhost:8888/user/123/orders

返回結果如下,符合我們的預期

{
    "orders": [
        {
            "id": "236802838635",
            "state": 1,
            "create_at": "2022-5-12 22:59:59"
        },
        {
            "id": "236802838636",
            "state": 1,
            "create_at": "2022-5-10 20:59:59"
        }
    ]
}

接下來我們再來實現建立訂單的邏輯,建立訂單首先需要檢視該商品的庫存是否足夠,所以在訂單模組中需要依賴商品模組。

type OrderCreateLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
    productLogic *ProductLogic
}

func NewOrderCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderCreateLogic {
    return &OrderCreateLogic{
        Logger:       logx.WithContext(ctx),
        ctx:          ctx,
        svcCtx:       svcCtx,
        productLogic: NewProductLogic(ctx, svcCtx),
    }
}

建立訂單的邏輯如下

const (
    success = 0
    failure = -1
)

func (l *OrderCreateLogic) OrderCreate(req *types.OrderCreateRequest) (*types.OrderCreateReply, error) {
    product, err := l.productLogic.productByID(req.ProductID)
    if err != nil {
        return nil, err
    }

    if product.Count > 0 {
        return &types.OrderCreateReply{Code: success}, nil
    }

    return &types.OrderCreateReply{Code: failure}, nil
}

依賴的商品模組邏輯如下

func (l *ProductLogic) Product(req *types.ProductRequest) (*types.ProductReply, error) {
    return l.productByID(req.ID)
}

func (l *ProductLogic) productByID(id int64) (*types.ProductReply, error) {
    return &types.ProductReply{
        ID:    id,
        Name:  "apple watch 3",
        Price: 3333.33,
        Count: 99,
    }, nil
}

以上可以看出使用 go-zero 開發單體服務還是非常簡單的,有助於我們快速開發上線,同時我們還做了模組的劃分,為以後做微服務的拆分也打下了基礎。

總結

通過以上的示例可以看出使用 go-zero 實現單體服務非常簡單,只需要定義 api 檔案,然後通過 goctl 工具就能自動生成專案程式碼,我們只需要在logic中填寫業務邏輯即可,這裡只是為了演示如何基於 go-zero 快速開發單體服務並沒有涉及資料庫和快取的操作,其實我們的 goctl 也可以一鍵生成 CRUD 程式碼和 cache 程式碼,對於開發單體服務來說可以起到事半功倍的效果。

並且針對不同的業務場景,定製化的需求也可以通過自定義模板來實現,還可以在團隊內通過遠端 git 倉庫共享自定義業務模板,可以很好的實現團隊協同。

專案地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zerostar 支援我們!

微信交流群

關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。

如果你有 go-zero 的使用心得文章,或者原始碼學習筆記,歡迎通過公眾號聯絡投稿!