---
title: "gRPC Server 準備 - Clean Architecture in Go"
date: 2025-04-04T00:00:00+08:00
publishDate: 2025-04-04T00:00:00+08:00
lastmod: 2025-10-19T16:08:45+08:00
tags: ["Golang","Clean Architecture","架構","經驗","gRPC"]
series: "clean-architecture-in-go"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/04/04/clean-architecture-in-go-setup-grpc/"
language: "zh-tw"
---


RESTful API 的版本已經實作完畢，我們接下來會實作 [gRPC](https://grpc.io/) 的版本，我們會透過這次的擴充，來了解為什麼 Controller 和 UseCase 會被切分開來。

<!--more-->

## 環境準備{#setup}

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

```bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```

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

為了方便測試 gRPC 的呼叫，可以安裝 [grpcurl](https://github.com/fullstorydev/grpcurl) 這個工具，這樣就能以類似 Curl 的方式測試我們搭建的 gRPC 伺服器。

## 實作伺服器{#implement-server}

要啟動 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` 的內容會像這樣。

```go
package main

import "log"

func main() {
	server, err := initialize()
	if err != nil {
		log.Fatalf("Error initializing handler: %v", err)
	}

	if err := server.Serve(); err != nil {
		log.Fatalf("Error starting server: %v", err)
	}
}
```

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

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

```go
var DefaultSet = wire.NewSet(
	NewServer,
)

type Server struct {
	grpc *grpc.Server
}

func NewServer() *Server {
	server := grpc.NewServer()

	reflection.Register(server)

	return &Server{
		grpc: server,
	}
}

func (s *Server) Serve() error {
	socket, err := net.Listen("tcp", ":8080")
	if err != nil {
		return err
	}

	return s.grpc.Serve(socket)
}
```

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

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

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

```bash
$ grpcurl -plaintext localhost:8080 list
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
```

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

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

## 註冊 Order Service {#register-order-service}

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

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

```protobuf
syntax = "proto3";

// NOTE: 改為你自己的 Package 名稱
option go_package = "github.com/elct9620/clean-architecture-in-go-2025/pkg/orderspb";

package orders;

service Order {
  rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse);
  rpc LookupOrder(LookupOrderRequest) returns (LookupOrderResponse);
}

message PlaceOrderRequest {
  string name = 1;
  repeated OrderItem items = 2;
}

message PlaceOrderResponse {
  string id = 1;
  string name = 2;
  repeated OrderItem items = 3;
}

message LookupOrderRequest {
  string id = 1;
}

message LookupOrderResponse {
  string id = 1;
  string name = 2;
  repeated OrderItem items = 3;
}

message OrderItem {
  string name = 1;
  int32 quantity = 2;
  int32 unit_price = 3;
}
```

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

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

```bash
protoc --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 的伺服器實作。

```go
// ...

var _ orderspb.OrderServer = &OrderServer{}

type OrderServer struct {
	orderspb.OrderServer
}

func NewOrderServer() *OrderServer {
	return &OrderServer{}
}

// PlaceOrder TODO
func (s *OrderServer) PlaceOrder(ctx context.Context, req *orderspb.PlaceOrderRequest) (*orderspb.PlaceOrderResponse, error) {
	return nil, nil
}

// LookupOrder TODO
func (s *OrderServer) LookupOrder(ctx context.Context, req *orderspb.LookupOrderRequest) (*orderspb.LookupOrderResponse, error) {
	return nil, nil
}

// ...
```

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

```go
// ...

var DefaultSet = wire.NewSet(
	NewOrderServer,
	NewServer,
)

// ...

func NewServer(
	orderServer *OrderServer,
) *Server {
	server := grpc.NewServer()

	orderspb.RegisterOrderServer(server, orderServer)
	reflection.Register(server)

	return &Server{
		grpc: server,
	}
}

// ...
```

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

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

```bash
$ grpcurl -plaintext localhost:8080 list
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
orders.Order
```

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

