# struct tag 與 reflect

> struct tag 靠 reflect 在執行期讀，Lookup 分得出空值與沒掛的 tag，reflect 會 panic 又慢，只在啟動期用，別進 hot path，這是宣告式路由的地基。

原文：https://sheng.page/posts/go-struct-tags-and-reflection/ · 發布：2025-03-23 · 作者：Sheng

struct tag 是 Go 留給工具的後門。gortex 的宣告式路由就靠 `reflect` 讀這些 tag，把一個 struct 變成整張路由表。這篇先把地基打好。

## struct tag 怎麼讀

struct tag 是欄位後面那串 raw string，慣例是空白分隔的 `key:"value"`：

```go
type User struct {
    Name string `json:"name" validate:"required"`
}
```

編譯器不解讀它，要靠 reflect 在執行期讀：

```go
f, _ := reflect.TypeOf(User{}).FieldByName("Name")
f.Tag.Get("json")            // "name"
v, ok := f.Tag.Lookup("xml") // "", false
```

`Get` 和 `Lookup` 的差別很關鍵：`Get` 對「沒這個 key」和「key 值為空」都回 `""`，分不出來；`Lookup` 多回一個 bool。gortex 判斷 `inject:""`（空值 tag）就是用 `Lookup`，否則會把「故意留空」誤判成「沒掛 tag」。

## reflect 三件套：Type / Value / Kind

- `reflect.Type`：靜態結構，有幾個欄位、欄位名、tag。
- `reflect.Value`：執行期的值，可讀可寫。
- `Kind`：底層分類（`Ptr`、`Struct`、`String`…），跟具名型別不同。`type UserID int` 的 Kind 是 `Int`。

```go
v := reflect.ValueOf(&User{})
v.Kind()         // reflect.Ptr
v.Elem().Kind()  // reflect.Struct；Elem() 解一層指標
t := v.Elem().Type()
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    _ = field.Tag.Get("json")
}
```

## 安全走訪與方法簽章檢查

reflect 出錯不回 error，是直接 panic。動手前先檢查：

- 先驗 `Kind`：別對非 struct 呼叫 `NumField()`、別對非指標呼叫 `Elem()`。
- 跳過 unexported 欄位：`field.CanInterface()`（或 `StructField.IsExported()`），否則取值會 panic。
- 要呼叫方法前，先 `MethodByName` 拿到，再看 `Method.Type` 的 `NumIn` / `NumOut` 確認簽章，才 `Call`。

這正是〈[Struct tag 宣告式路由：reflect 實戰](/posts/gortex-struct-tag-routing/)〉路由註冊在做的事：掃欄位找 `GET` / `POST` 方法、檢查簽章、才註冊。

## reflect 的代價

reflect 慢（繞過靜態分派）、繞過編譯期型別檢查、又會在執行期 panic。原則只有一條：**在啟動／設定期用，別放進 hot path**。gortex 就是這樣切：啟動時用 reflect 掃路由換開發體驗，production build 改用 codegen，每條請求完全不碰 reflect（見宣告式路由那篇與〈[零分配的秘密：Context pool 與 smartParams](/posts/gortex-zero-allocation-context/)〉）。

## Effective Go 精選：blank identifier、Data

blank identifier `_` 在 Effective Go 有整節（[The blank identifier](https://go.dev/doc/effective_go#blank)）：吃掉用不到的回傳值、`for range` 只要 index、以及介面檢查：

```go
var _ io.Writer = (*MyType)(nil) // 編譯期確認 *MyType 有實作 io.Writer，沒有就編不過
```

[Data](https://go.dev/doc/effective_go#data) 那節講配置：`new(T)` 配一塊歸零記憶體回 `*T`，`make` 專給 slice / map / channel。`reflect.New` 等價於 `new`，gortex 的宣告式路由自動初始化 nil handler 欄位就靠它。


## 補充筆記

- struct tag 是欄位後的 raw string，reflect 用 `Get` / `Lookup` 讀；`Lookup` 能分辨「沒掛」與「掛了空值」。
- 三件套 Type / Value / Kind；Kind 看底層分類，跟具名型別無關。
- reflect 會 panic：先檢查 Kind、跳過 unexported、方法先驗簽章再 call。
- reflect 慢又不安全：只在啟動期用，請求路徑別碰。
- struct tag + reflect 就是 gortex 宣告式路由那篇的地基。

延伸：本系列接著用這些觀念拆 gortex 框架，原始碼見 [yshengliao/gortex](https://github.com/yshengliao/gortex)。

---

> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 環境 Go 1.24（當時最新 release）· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃，新漆未乾。