Go Struct Tag and reflect

Go Struct Tag 是什麼?如何透過 reflect 動態處理欄位?

前言

type Restaurant struct {
Name string
RestaurantId string `bson:"restaurant_id,omitempty"`
Cuisine string `bson:"cuisine,omitempty"`
Address interface{} `bson:"address,omitempty"`
Borough string `bson:"borough,omitempty"`
Grades []interface{} `bson:"grades,omitempty"`
}

最近在與 MongoDB 互動時發現 Struct 欄位結尾有一段語法不是很熟悉,這篇文章探討 Struct Tag 存在的原因以及解決什麼問題。

Struct Tag

用於標示 Struct 欄位的元資料(用於描述資料的資料)

Struct Tag 通常會在 runtime 執行時用做:「格式驗證」、「資料序列化」、「資料庫欄位應對」……等用途,可用於存儲任何資料沒有限制。與其命令式的操縱資料,透過宣告式的方式來描述資料格式是 Struct Tag 的主要用途。

用途範例
資料驗證validate:"required,min=3"
欄位權限auth:"admin_only"
序列化格式csv:"username"
對 ORM 的描述gorm:"primaryKey"
API export 控制expose:"false"

Struct Tag 格式

Go 標準對 struct tag 的格式 有慣例但非強制

key1:"value1" key2:"value2"

value 裡常見分隔符:

  • , → 參數列表,例如:json:"name,omitempty"
  • : → key-value,例如:validate:"min=3"
  • | → 多條規則分隔,例如:rule:"a|b|c"(自訂風格)

Struct Tag 只是一串字串用於描述欄位,並不包含解析的邏輯,需要自己實踐透過 reflect 官方套件。

reflect 套件操作

reflect 套件用於觀察並修改數值於 runtime

基於 Go 是靜態型別的語言,在編譯期就決定所有型別的語言,透過 relfect 套件讀取 Struct Tag 並依其內容決定程式邏輯。

package main
import (
"fmt"
"reflect"
)
func ValidateRequired(s any) error {
v := reflect.ValueOf(s)
t := reflect.TypeOf(s)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("validate")
if tag == "required" {
value := v.Field(i)
if value.IsZero() {
return fmt.Errorf("field '%s' is required", field.Name)
}
}
}
return nil
}
func main() {
type User struct {
Name string `validate:"required"`
}
u1 := User{Name: "John"}
fmt.Println(ValidateRequired(u1)) // <nil>
u2 := User{}
fmt.Println(ValidateRequired(u2)) // field 'Name' is required
}

總結

所以回到原先的問題,可以發現 MongoDB Driver for Go 使用 Struct Tags🔗 背後其實是為了將資料應對為 bson 才需要額外添加這些 struct tag。

延伸閱讀