---
title: "輸入檢查 - Clean Architecture in Go"
date: 2025-04-18T00:00:00+08:00
publishDate: 2025-04-18T00:00:00+08:00
lastmod: 2024-10-07T20:05:43+08:00
tags: ["Golang","Clean Architecture","架構","經驗"]
series: "clean-architecture-in-go"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/04/18/clean-architecture-in-go-validation/"
language: "zh-tw"
---


在前面的案例中，我們並沒有考量到「輸入檢查」這件事情，因此在 gRPC 的案例中，我們發現即使沒有填寫 `Name` 也可以順利建立訂單，這並不符合我們的期待。

<!--more-->

## 驗證時機{#timing-to-validate}

驗證（Validate）到底該在什麼時機點進行，一直以來都沒有一個容易判斷的方式。有些框架會設計在 Model 上，也就是在持久化時才進行檢查，有些則是會附加在 Controller 上，做為參數輸入時就要確認。

然而，我們在處理的過程中會出現好幾種不同的情境。

第一種是編碼（Marshalling）的處理，像是 Golang 將 JSON 反序列化（Deserialize）為一個 Struct 時，如果跟結構的型別無法對應，那麼會發生錯誤。

第二種則是輸入數值的處理，像是 `Name` 這個必填欄位沒有填寫，價格不能為 `0` 這類情境，也是我們比較常在 Web 框架上會看到的類型。

第三種則是行為上的處理，我們在 `Order` 的 `AddItem()` 方法，設計了一個檢查商品名稱不能重複的確認，這種檢查通常會跟我們的狀態有關，只有 Entity 本身才能判斷。

也就是說，以 Clean Architecture 的設計來看，我們的驗證處理分別在 Adapter 的 Controller、Repository 會發生關於「編碼」的檢查，到了 UseCase 則有資訊是否滿足的檢查，以及 Entity 的狀態是否正確的檢查。

那麼，訂單沒有填入姓名、商品的單價和數量不對，應該是第二種類型。

## Validator Package

要在 UseCase 加入驗證，我們應該要依照 UseCase 不會知道細節的原則，因此在 UseCase 定義一個 `Validator` 介面，描述驗證的需求。

```go
// internal/usecase/usecase.go

type Validator interface {
	Validate(ctx context.Context, input any) error
}
```

修改 Place Order UseCase 加入驗證的步驟，這裡我們選擇使用 [validator](https://github.com/go-playground/validator) 這個套件，利用 Golang 的 Struct Tags 就不會有直接的依賴關係。

```go
// ...
type PlaceOrderItem struct {
	Name      string `validate:"required"`
	Quantity  int    `validate:"required,gte=1"`
	UnitPrice int    `validate:"required,gte=1"`
}

type PlaceOrderInput struct {
	Name  string           `validate:"required"`
	Items []PlaceOrderItem `validate:"required,gt=0,dive,required"`
}

// ...

type PlaceOrder struct {
	orders    OrderRepository
	tokens    TokenRepository
	validator Validator
}

func NewPlaceOrder(orders OrderRepository, tokens TokenRepository, validator Validator) *PlaceOrder {
	return &PlaceOrder{
		orders:    orders,
		tokens:    tokens,
		validator: validator,
	}
}

func (u *PlaceOrder) Execute(ctx context.Context, input *PlaceOrderInput) (*PlaceOrderOutput, error) {
	if err := u.validator.Validate(ctx, input); err != nil {
		return nil, err
	}

	// ...
}
```

像這樣，我們就可以運行這個 UseCase 之前確定必填的欄位是否都有填寫，以及數量、單價不能為 `0`。

使用 Golang 的 Struct Tags 類似於 Java 的 Annotation 都是透過語言的反射（Reflection）機制來達成的效果，因為是 Metadata 的形式，也就不會直接依賴於某個套件。

> 因為不同套件使用的 Struct Tags 不同，因此還是一定程度受到影響，然而當我們要抽換時只要使用相同方式就能夠做替換。

最後，我們只需要在 `internal/validator` 加入簡單的封裝即可。

```go
// internal/validator/validator.go

// ...

type Validator struct {
	validate *validator.Validate
}

func New() *Validator {
	return &Validator{
		validate: validator.New(),
	}
}

func (v *Validator) Validate(ctx context.Context, i any) error {
	return v.validate.StructCtx(ctx, i)
}
```

之後只需要利用像是 `wire` 這類工具，針對有 `Validator` 介面需求的 UseCase 注入這個物件，就可以快速地提供驗證機制。

## 難以統整{#hard-to-integrate}

雖然我們將驗證的發生時機點設定在 UseCase 上，但是還是有很多不同的情境。以 OpenAPI 生態系來說，我們是可以在 HTTP 請求階段就做處理。

以 [kin-openapi](https://github.com/getkin/kin-openapi) 這個套件來說，他提供了一個 `ValidateRequest` 的機制，可以檢查發過來的 HTTP 請求和 API 文件是否一致，包含 HTTP Status Code 和支援的內容類型（Content-Type）都可以處理。

這個做法可以很好的改善開發過程中對 API 處理的失誤，但裡面很大一部分的處理都跟 UseCase 的驗證重疊，假設我們使用了這個機制進行驗證，那麼 UseCase 的驗證基本上是不會觸發的。但是，在 gRPC 的角度來看 Protobuf 定位是跟 JSON 差不多的角色，因此 Protobuf 本身無法做任何額外的驗證方式標記。

這就造成了我們可能在使用 OpenAPI 方式設計 RESTful API 時沒有注意到驗證問題，當支援了 gRPC 協定時，就因為各種輸入驗證的缺少而造成問題。

除此之外，OpenAPI 驗證的訊息跟我們自己封裝的 Validator 也會有差異，這也會造成後續在測試時因為不一致而導致的混亂，這些都會是我們未來會需要去處理和改善的問題。

