
gRPC Server 準備 - Clean Architecture in Go
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/http
和 cmd/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 上。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - Clean Architecture in Go
- 目標設定 - Clean Architecture in Go
- wire 的依賴注入 - Clean Architecture in Go
- 案例說明 - Clean Architecture in Go
- 操作介面設計 - Clean Architecture in Go
- Place Order 實作 Controller 部分 - Clean Architecture in Go
- Place Order 實作 Entity 部分 - Clean Architecture in Go
- Place Order 實作 Repository 部分 - Clean Architecture in Go
- Lookup Order 功能 - Clean Architecture in Go
- Tokenization 機制設計 - Clean Architecture in Go
- 在 Place Order 實作 Token 機制 - Clean Architecture in Go
- 在 Lookup Order 實作 Token 機制 - Clean Architecture in Go
- Token 內容加密 - Clean Architecture in Go
- gRPC Server 準備 - Clean Architecture in Go
- gRPC Server 實作 - Clean Architecture in Go
- 輸入檢查 - Clean Architecture in Go
- 資料庫抽換 - BoltDB - Clean Architecture in Go
- 資料庫抽換 - SQLite(一) - Clean Architecture in Go
- 資料庫抽換 - SQLite(二) - Clean Architecture in Go
- 實作 LRU Cache - Clean Architecture in Go
- 反思:必要性 - Clean Architecture in Go