蒼時弦也
蒼時弦也
資深軟體工程師
發表於

gRPC Server 準備 - Clean Architecture in Go

這篇文章是 Clean Architecture in Go 系列的一部分,你可以透過 Leanpub 提前閱讀內容。

RESTful API 的版本已經實作完畢,我們接下來會實作 gRPC 的版本,我們會透過這次的擴充,來了解為什麼 Controller 和 UseCase 會被切分開來。

環境準備

要使用 gRPC 我們需要可以協助我們生成 Protobuf 的工具,因此需要安裝 protoc 命令,以及用於 Golang 的 Codegen 擴充。

1go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
2go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

安裝完畢後,可以用 protoc --version 來確認是否正確安裝,如果找不到這個命令,可以檢查 Golang 的 Binary 目錄是否有正確設定。

為了方便測試 gRPC 的呼叫,可以安裝 grpcurl 這個工具,這樣就能以類似 Curl 的方式測試我們搭建的 gRPC 伺服器。

實作伺服器

要啟動 gRPC 的伺服器並不複雜,跟 HTTP Server 的方法類似,我們先前已經透過 oapi-codegen 來生成大部分 HTTP Server 的實作,因此在 internal/api/rest 套件中會透過對應的 HTTP Server 套件封裝成 *rest.Server 來提供啟動伺服器的機制。

在這系列的設計中,我們會預期 HTTP Server 和 gRPC Server 是不同的執行檔,因此會各別放在 cmd/httpcmd/grpc 兩個套件下。

在這兩個套件,我們會透過 wire 一次性的生成所有依賴,因此 main.go 的內容會像這樣。

 1package main
 2
 3import "log"
 4
 5func main() {
 6	server, err := initialize()
 7	if err != nil {
 8		log.Fatalf("Error initializing handler: %v", err)
 9	}
10
11	if err := server.Serve(); err != nil {
12		log.Fatalf("Error starting server: %v", err)
13	}
14}

initialize() 這個方法,會回傳一個我們在 internal/api/grpc 套件中所實作的 *grpc.Server 結構。

打開 internal/api/grpc/grpc.go 來實作這個結構。

 1var DefaultSet = wire.NewSet(
 2	NewServer,
 3)
 4
 5type Server struct {
 6	grpc *grpc.Server
 7}
 8
 9func NewServer() *Server {
10	server := grpc.NewServer()
11
12	reflection.Register(server)
13
14	return &Server{
15		grpc: server,
16	}
17}
18
19func (s *Server) Serve() error {
20	socket, err := net.Listen("tcp", ":8080")
21	if err != nil {
22		return err
23	}
24
25	return s.grpc.Serve(socket)
26}

實作上基本上跟 HTTP Server 是類似的,假設希望可以在同一個執行檔切換,可以考慮將 Serve()ServeHTTP() 用介面統一,再利用 flag 套件來根據傳入的選項判斷。

這邊並沒有額外實作設定的機制讓我們可以修改 net.Listen 監聽的埠號,在正式的環境中建議加入像是 *Config 的設定,允許修改。

此時我們就可以用 go run ./cmd/grpc 啟動伺服器,並且用 grpcurl 來確認是否正常。

1$ grpcurl -plaintext localhost:8080 list
2grpc.reflection.v1.ServerReflection
3grpc.reflection.v1alpha.ServerReflection

因為我們有加入 reflection.Register(server) 這個設定,讓 gRPC 可以自動偵測支援的服務,開發階段用於除錯會非常方便,正式環境則可以選擇關閉。

如果使用 go run ./cmd/grpc 時發生錯誤,可以先使用 go mod tidy 來安裝缺少的 gRPC 相關套件

註冊 Order Service

現在的 gRPC Server 雖然能夠啟動,但無法呼叫原本 Order Service 的 Place Order 和 Lookup Order 兩個功能,因此我們要將服務註冊進去。

pkg/orderspb/orders.proto 加入以下內容

 1syntax = "proto3";
 2
 3// NOTE: 改為你自己的 Package 名稱
 4option go_package = "github.com/elct9620/clean-architecture-in-go-2025/pkg/orderspb";
 5
 6package orders;
 7
 8service Order {
 9  rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse);
10  rpc LookupOrder(LookupOrderRequest) returns (LookupOrderResponse);
11}
12
13message PlaceOrderRequest {
14  string name = 1;
15  repeated OrderItem items = 2;
16}
17
18message PlaceOrderResponse {
19  string id = 1;
20  string name = 2;
21  repeated OrderItem items = 3;
22}
23
24message LookupOrderRequest {
25  string id = 1;
26}
27
28message LookupOrderResponse {
29  string id = 1;
30  string name = 2;
31  repeated OrderItem items = 3;
32}
33
34message OrderItem {
35  string name = 1;
36  int32 quantity = 2;
37  int32 unit_price = 3;
38}

可以理解為跟我們撰寫 OpenAPI 文件相同的目的,都是要讓 Code Generator 可以理解我們的標準,自動生成對應的內容,上述的內容只需要根據已經撰寫好的 OpenAPI 文件改寫即可。

後續只要交給 protoc 命令幫我們生成 Server 的介面,在進行實作即可。

1protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pkg/orderspb/orders.proto

此時我們就可以在 internal/api/grpc/grpc.go 加入 Order Service 的伺服器實作。

 1// ...
 2
 3var _ orderspb.OrderServer = &OrderServer{}
 4
 5type OrderServer struct {
 6	orderspb.OrderServer
 7}
 8
 9func NewOrderServer() *OrderServer {
10	return &OrderServer{}
11}
12
13// PlaceOrder TODO
14func (s *OrderServer) PlaceOrder(ctx context.Context, req *orderspb.PlaceOrderRequest) (*orderspb.PlaceOrderResponse, error) {
15	return nil, nil
16}
17
18// LookupOrder TODO
19func (s *OrderServer) LookupOrder(ctx context.Context, req *orderspb.LookupOrderRequest) (*orderspb.LookupOrderResponse, error) {
20	return nil, nil
21}
22
23// ...

最後讓原本的 *grpc.Server 在初始化時註冊進去即可。

 1// ...
 2
 3var DefaultSet = wire.NewSet(
 4	NewOrderServer,
 5	NewServer,
 6)
 7
 8// ...
 9
10func NewServer(
11	orderServer *OrderServer,
12) *Server {
13	server := grpc.NewServer()
14
15	orderspb.RegisterOrderServer(server, orderServer)
16	reflection.Register(server)
17
18	return &Server{
19		grpc: server,
20	}
21}
22
23// ...

由此可見使用 wire 來進行依賴注入,可以很大的減少我們手動去維護這些物件之間的依賴,只需要定義好 Provider(如:NewOrderServer)就能自動插入到我們預期的位置。

那麼,重新啟動伺服器後再次使用 grpcurl 就可以看到我們註冊進去的服務。

1$ grpcurl -plaintext localhost:8080 list
2grpc.reflection.v1.ServerReflection
3grpc.reflection.v1alpha.ServerReflection
4orders.Order

接下來我們就可以利用 UseCase 快速的將原有的功能移植到 gRPC 上。