---
title: "gRPC Server 實作 - Clean Architecture in Go"
date: 2025-04-11T00:00:00+08:00
publishDate: 2025-04-11T00:00:00+08:00
lastmod: 2024-10-07T20:05:43+08:00
tags: ["Golang","Clean Architecture","架構","經驗","gRPC"]
series: "clean-architecture-in-go"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/04/11/clean-architecture-in-go-implement-grpc/"
language: "zh-tw"
---


在上一階段我們完成了可以運行的 gRPC Server 接下來就可以將原本 RESTful API 上的實作移植到 gRPC 上，讓我們可以使用 gRPC 來操作這些功能。

<!--more-->

## 前置準備{#prepare}

因為我們將 RESTful API 常用的 Controller 定位在「處理輸入」的角色，所以 Controller 只有對 JSON 解析相關的處理，實際的商業邏輯都是放在 UseCase 裡面。

基於這樣的理由，我們只需要讓 `*grpc.OrderServer` 跟 `*rest.Api` 一樣參考這些 UseCase 就可以重現相同的功能。

```go
// internal/api/grpc/grpc.go

var DefaultSet = wire.NewSet(
	wire.Struct(new(OrderServer), "*"),
	NewServer,
)

var _ orderspb.OrderServer = &OrderServer{}

type OrderServer struct {
	orderspb.OrderServer `wire:"-"`
	PlaceOrderUsecase    *usecase.PlaceOrder
	LookupOrderUsecase   *usecase.LookupOrder
}

// ...
```

因為 `wire` 的依賴注入會掃描所有的 Field，我們需要用 `wire:"-"` 來表示不用對這個 Field 注入，原本的 `NewOrderServer` 也可以改為 `wire.Struct` 的方式替換掉。

使用 `wire` 命令重新生成依賴注入的檔案後，我們就可以進行後續的實作。

## Lookup Order

我們先從比較單純的 Lookup Order 來處理，實作的方式跟 RESTful API 基本上相同。將原本 `internal/api/grpc/grpc.go` 的方法移動到 `internal/api/grpc/lookup_order.go` 加入以下實作。

```go
// ...

func (s *OrderServer) LookupOrder(ctx context.Context, req *orderspb.LookupOrderRequest) (*orderspb.LookupOrderResponse, error) {
	out, err := s.LookupOrderUsecase.Execute(ctx, &usecase.LookupOrderInput{
		Id: req.Id,
	})

	if err != nil {
		return nil, err
	}

	res := buildLookupOrderResponse(out)

	return res, nil
}

func buildLookupOrderResponse(out *usecase.LookupOrderOutput) *orderspb.LookupOrderResponse {
	outItems := make([]*orderspb.OrderItem, 0, len(out.Items))
	for _, item := range out.Items {
		outItems = append(outItems, &orderspb.OrderItem{
			Name:      item.Name,
			Quantity:  int32(item.Quantity),
			UnitPrice: int32(item.UnitPrice),
		})
	}

	return &orderspb.LookupOrderResponse{
		Id:    out.Id,
		Name:  out.Name,
		Items: outItems,
	}
}
```

除了將 gRPC 生成的資料結構轉換為我們 UseCase 的結構外，和使用 RESTfult API 幾乎沒有差異，透過這樣的手法我們讓這些方法可以很輕易地轉換到不同的協定上，或者在各類情境中使用。

## Place Order

基本上和 Lookup Order 相同，我們將方法移動到 `internal/api/grpc/place_order.go` 中，並且加入以下實作。

```go
func (s *OrderServer) PlaceOrder(ctx context.Context, req *orderspb.PlaceOrderRequest) (*orderspb.PlaceOrderResponse, error) {
	input := buildPlaceOrderInput(req)

	out, err := s.PlaceOrderUsecase.Execute(ctx, input)
	if err != nil {
		return nil, err
	}

	res := buildPlaceOrderResponse(out)

	return res, nil
}

func buildPlaceOrderInput(req *orderspb.PlaceOrderRequest) *usecase.PlaceOrderInput {
	inItems := make([]usecase.PlaceOrderItem, 0, len(req.Items))
	for _, item := range req.Items {
		inItems = append(inItems, usecase.PlaceOrderItem{
			Name:      item.Name,
			Quantity:  int(item.Quantity),
			UnitPrice: int(item.UnitPrice),
		})
	}

	return &usecase.PlaceOrderInput{
		Name:  req.Name,
		Items: inItems,
	}
}

func buildPlaceOrderResponse(out *usecase.PlaceOrderOutput) *orderspb.PlaceOrderResponse {
	outItems := make([]*orderspb.OrderItem, 0, len(out.Items))
	for _, item := range out.Items {
		outItems = append(outItems, &orderspb.OrderItem{
			Name:      item.Name,
			Quantity:  int32(item.Quantity),
			UnitPrice: int32(item.UnitPrice),
		})
	}

	return &orderspb.PlaceOrderResponse{
		Id:    out.Id,
		Name:  out.Name,
		Items: outItems,
	}
}
```

因為輸入跟輸出的內容比較多，因此需要花比較多力氣在處理上除此之外沒什麼不同。

最後，我們可以啟動 gRPC Server 使用 `grpcurl` 命令來測試看看。

```bash
$ grpcurl -plaintext localhost:8080 orders.Order.PlaceOrder
{
  "id": "e1454ea1-f31c-415a-9df0-2f89dd8ef85a"
}
```

好像哪裡有點奇怪，跟 RESTful API 的行為似乎有點不同，我們接下來會來探討一下這個問題。

