5 分鐘搞定 Go 自定義結構體標籤

本文介紹瞭如何通過自定義結構體標籤實現對結構體字段值的自定義處理,並討論了該方法的優缺點。原文:Struct Tags in Go: Implementing Custom Tag Functionality[1]

Go 的 struct 標籤提供了一種爲 struct 字段定義元數據的方法,允許開發人員指定在序列化或驗證等操作期間如何處理這些字段。一個常見用例是 json 標籤,告訴 Go 的 JSON 包如何將結構字段映射到 JSON 鍵。例如:

type User struct {
    ID int `json:"id"`
    Name string `json:"name"`
    Email string `json:"email"`
}

本文將探索如何創建自定義結構體標籤來實現自定義功能。我們將使用淨化(sanitizing)HTML 內容的用例 —— 在許多 web 應用中,這是防止跨站腳本(XSS)攻擊的重要功能。我們將學習如何定義自定義標記,實現自定義標籤邏輯,並無縫集成到 Go 結構體中。

什麼是結構體標籤?

在 Go 中,結構體標籤是在結構體中字段聲明之後的反引號 `` 中定義的元數據。可以使用 reflect 包處理這些標記,它允許在運行時對 Go 類型進行處理。struct 標籤的一般格式如下所示:

type StructName struct {
    FieldName FieldType `tagName:"value"`
}

可以在一個字段中定義多個標籤,例如,json和一個用於淨化的自定義標籤sanitize

type Comment struct {
    Body string `json:"body" sanitize:"stripall"`
}

在這個例子中,stripall可以是一個自定義的清理指令,指示在存儲或處理字段之前,應該刪除所有 HTML 內容。

通過 Struct 標籤實現 HTML 淨化

我們構建一個自定義解決方案,通過 struct 標籤從 struct 字段中淨化 HTML 內容。我們的目標是創建一個函數,它可以檢查結構體的每個字段,並根據標籤值進行必要的淨化。

步驟 1:定義自定義標籤值

sanitize標籤將指示代碼根據其值刪除所有 HTML 標籤或允許某些安全的 HTML 標籤:

結構體看起來像這樣:

type Comment struct {
    Body string `json:"body" sanitize:"stripall"`
    Title string `json:"title" sanitize:"safeugc"`
}

步驟 2:編寫淨化函數

編寫SanitizeStruct函數,通過 Go 的reflect包來處理結構體標籤並清理字段:

func getCleansedValue(tag string, value string) (string, error) {
var cleanedValue string
switch TagValue(tag) {
case StripAll:
  cleanedValue = config.StripAll(value)
case SafeUGC:
  cleanedValue = config.SafeUGC(value)
default:
return"", ErrInvalidTagValue
 }

return cleanedValue, nil
}

// SanitizeStruct takes a struct and sanitizes it based on `sanitize` tags
func SanitizeStruct(s interface{}) error {
 val := reflect.ValueOf(s).Elem()
 typ := val.Type()

for i := 0; i < val.NumField(); i++ {
  field := val.Field(i)
  fieldType := typ.Field(i)
  fieldKind := field.Kind()

  tag := fieldType.Tag.Get("sanitize")
if tag == "" {
   if fieldKind == reflect.Struct {
    if err := SanitizeStruct(field.Addr().Interface()); err != nil {
     return err
    }
   }
   continue
  } elseif fieldKind == reflect.Struct {
   return ErrInvalidPropertyType
  } elseif fieldKind != reflect.String && !(fieldKind == reflect.Slice && field.Type().Elem().Kind() == reflect.String) {
   return ErrInvalidPropertyType
  }

if fieldKind == reflect.Slice {
   for j := 0; j < field.Len(); j++ {
    cleanedValue, err := getCleansedValue(tag, field.Index(j).String())
    if err != nil {
     return err
    }
    field.Index(j).SetString(cleanedValue)
   }
  } else {
   cleanedValue, err := getCleansedValue(tag, field.String())
   if err != nil {
    return err
   }
   field.SetString(cleanedValue)
  }
 }

returnnil
}

然後,可以很方便的利用上述功能:

func main() {
 comment:= &Comment{
  Body: "<script>alert('xss')</script>",
  Title: "<b>Bold Title</b>",
 }

 err := SanitizeStruct(comment)
if err != nil {
  fmt.Println("Error:", err)
 }

 fmt.Printf("Sanitized Comment: %+v\n", comment)
}

本例中:

輸出如下:

Sanitized Comment: &{Body: Title:<b>Bold Title</b>}

什麼時候不適合用結構體標籤

對於更復雜的邏輯或需要動態行爲的場景,結構體標籤並不總是最佳選擇。由於結構體標籤是靜態的,並在運行時通過反射進行計算,因此在業務邏輯可能頻繁更改或需要有條件的應用時缺乏靈活性。此外,標籤不適合實現更復雜的驗證工作流、錯誤處理或依賴外部因素的行爲,因爲當邏輯變得更復雜時,反射可能會變得更慢,更難以調試。對於更高級或不斷變化的需求,將這種邏輯嵌入到代碼中的其他地方,如專用函數或中間件中,可能是更好的方法。

結論

Go 的 struct 標籤提供了一種靈活的聲明式方法,可以直接將特殊字段邏輯添加到數據結構中。通過將自定義結構體標籤與反射相結合,可以創建可重用的解決方案,並且可以輕鬆的跨結構體屬性實現。請注意,通過反射來檢查結構體標籤會增加開銷,所以在性能敏感的場合要小心使用這種方法!


參考資料

[1] 

Struct Tags in Go: Implementing Custom Tag Functionality: https://medium.com/@ullauri.byron/struct-tags-in-go-implementing-custom-tag-functionality-b32c3c87423d?source=email-fcfdcee87c4a-1733682034928-digest.reader--b32c3c87423d----7-98------------------c5ba7540_a4c5_4f29_9009_de234e6e939b-1

請在微信客戶端打開

請在微信客戶端打開

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QlnDypYyz30gm18ejNWwTA