golang 如何驗證struct欄位的數據格式

来源:http://www.cnblogs.com/zhangyachen/archive/2017/12/12/8030245.html
-Advertisement-
Play Games

本文同時發表在 "https://github.com/zhangyachen/zhangyachen.github.io/issues/125" 假設我們有如下結構體: 我們需要對結構體內的欄位進行驗證合法性: Id的值在某一個範圍內。 Name的長度在某一個範圍內。 Email格式正確。 我們可 ...


本文同時發表在https://github.com/zhangyachen/zhangyachen.github.io/issues/125

假設我們有如下結構體:

type User struct {
    Id    int    
    Name  string 
    Bio   string 
    Email string 
}

我們需要對結構體內的欄位進行驗證合法性:

  • Id的值在某一個範圍內。
  • Name的長度在某一個範圍內。
  • Email格式正確。

我們可能會這麼寫:

user := User{
        Id:    0,
        Name:  "superlongstring",
        Bio:   "",
        Email: "foobar",
}

if user.Id < 1 && user.Id > 1000 {
    return false
}
if len(user.Name) < 2 && len(user.Name) > 10 {
    return false
}
if !validateEmail(user.Email) {
    return false
}

這樣的話代碼比較冗餘,而且如果結構體新加欄位,還需要再修改驗證函數再加一段if判斷。這樣代碼比較冗餘。我們可以藉助golang的structTag來解決上述的問題:

type User struct {
    Id    int    `validate:"number,min=1,max=1000"`
    Name  string `validate:"string,min=2,max=10"`
    Bio   string `validate:"string"`
    Email string `validate:"email"`
}

validate:"number,min=1,max=1000"就是structTag。如果對這個比較陌生的話,看看下麵這個:


type User struct {
    Id        int       `json:"id"`
    Name      string    `json:"name"`
    Bio       string    `json:"about,omitempty"`
    Active    bool      `json:"active"`
    Admin     bool      `json:"-"`
    CreatedAt time.Time `json:"created_at"`
}

寫過golang的基本都用過json:xxx這個用法,json:xxx其實也是一個structTag,只不過這是golang幫你實現好特定用法的structTag。而validate:"number,min=1,max=1000"是我們自定義的structTag。

實現思路

image

我們定義一個介面Validator,定義一個方法Validate。再定義有具體意義的驗證器例如StringValidatorNumberValidatorEmailValidator來實現介面Validator
這裡為什麼要使用介面?假設我們不使用介面代碼會怎麼寫?

if tagIsOfNumber(){
        validator := NumberValidator{}
}else if tagIsOfString() {
        validator := StringValidator{}
}else if tagIsOfEmail() {
        validator := EmailValidator{}
}else if tagIsOfDefault() {
        validator := DefaultValidator{}
}

這樣的話判斷邏輯不能寫在一個函數中,因為返回值validator會因為structTag的不同而不同,而且validator也不能當做函數參數做傳遞。而我們定義一個介面,所有的validator都去實現這個介面,上述的問題就能解決,而且邏輯更加清晰和緊湊。
關於介面的使用可以看下標準庫的io Writer,Writer是個interface,只有一個方法Writer:

type Writer interface {
    Write(p []byte) (n int, err error)
}

而輸出函數可以直接調用參數的Write方法即可,無需關心到底是寫到文件還是寫到標準輸出:

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintf(format, a)
    n, err = w.Write(p.buf)      //調用Write方法
    p.free()
    return
}

//調用
Fprintf(os.Stdout, format, a...)    //標準輸出
Fprintf(os.Stderr, msg+"\n", args...)   //標準錯誤輸出

var buf bytes.Buffer
Fprintf(&buf, "[")    //寫入到Buffer的緩存中

言歸正傳,我們看下完整代碼,代碼是Custom struct field tags in Golang中給出的:

package main

import (
    "fmt"
    "reflect"
    "regexp"
    "strings"
)

const tagName = "validate"

//郵箱驗證正則
var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)

//驗證介面
type Validator interface {
    Validate(interface{}) (bool, error)
}

type DefaultValidator struct {
}

func (v DefaultValidator) Validate(val interface{}) (bool, error) {
    return true, nil
}

type StringValidator struct {
    Min int
    Max int
}

func (v StringValidator) Validate(val interface{}) (bool, error) {
    l := len(val.(string))

    if l == 0 {
        return false, fmt.Errorf("cannot be blank")
    }

    if l < v.Min {
        return false, fmt.Errorf("should be at least %v chars long", v.Min)
    }

    if v.Max >= v.Min && l > v.Max {
        return false, fmt.Errorf("should be less than %v chars long", v.Max)
    }

    return true, nil
}


type NumberValidator struct {
    Min int
    Max int
}

func (v NumberValidator) Validate(val interface{}) (bool, error) {
    num := val.(int)

    if num < v.Min {
        return false, fmt.Errorf("should be greater than %v", v.Min)
    }

    if v.Max >= v.Min && num > v.Max {
        return false, fmt.Errorf("should be less than %v", v.Max)
    }

    return true, nil
}

type EmailValidator struct {
}

func (v EmailValidator) Validate(val interface{}) (bool, error) {
    if !mailRe.MatchString(val.(string)) {
        return false, fmt.Errorf("is not a valid email address")
    }
    return true, nil
}

func getValidatorFromTag(tag string) Validator {
    args := strings.Split(tag, ",")

    switch args[0] {
    case "number":
        validator := NumberValidator{}
        //將structTag中的min和max解析到結構體中
        fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
        return validator
    case "string":
        validator := StringValidator{}
        fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
        return validator
    case "email":
        return EmailValidator{}
    }

    return DefaultValidator{}
}

func validateStruct(s interface{}) []error {
    errs := []error{}

    v := reflect.ValueOf(s)

    for i := 0; i < v.NumField(); i++ {
        //利用反射獲取structTag
        tag := v.Type().Field(i).Tag.Get(tagName)

        if tag == "" || tag == "-" {
            continue
        }

        validator := getValidatorFromTag(tag)

        valid, err := validator.Validate(v.Field(i).Interface())
        if !valid && err != nil {
            errs = append(errs, fmt.Errorf("%s %s", v.Type().Field(i).Name, err.Error()))
        }
    }

    return errs
}

type User struct {
    Id    int    `validate:"number,min=1,max=1000"`
    Name  string `validate:"string,min=2,max=10"`
    Bio   string `validate:"string"`
    Email string `validate:"email"`
}

func main() {
    user := User{
        Id:    0,
        Name:  "superlongstring",
        Bio:   "",
        Email: "foobar",
    }

    fmt.Println("Errors:")
    for i, err := range validateStruct(user) {
        fmt.Printf("\t%d. %s\n", i+1, err.Error())
    }
}

代碼很好理解,結構也很清晰,不做過多解釋了^_^

github上其實已經有現成的驗證包了govalidator,支持內置支持的驗證tag和自定義驗證tag:

package main

import (
    "github.com/asaskevich/govalidator"
    "fmt"
    "strings"
)

type Server struct {
    ID         string `valid:"uuid,required"`
    Name       string `valid:"machine_id"`
    HostIP     string `valid:"ip"`
    MacAddress string `valid:"mac,required"`
    WebAddress string `valid:"url"`
    AdminEmail string `valid:"email"`
}

func main() {
    server := &Server{
        ID:         "123e4567-e89b-12d3-a456-426655440000",
        Name:       "IX01",
        HostIP:     "127.0.0.1",
        MacAddress: "01:23:45:67:89:ab",
        WebAddress: "www.example.com",
        AdminEmail: "[email protected]",
    }

    //自定義tag驗證函數
    govalidator.TagMap["machine_id"] = govalidator.Validator(func(str string) bool {
        return strings.HasPrefix(str, "IX")
    })

    if ok, err := govalidator.ValidateStruct(server); err != nil {
        panic(err)
    } else {
        fmt.Printf("OK: %v\n", ok)
    }
}

參考資料:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1單例模式2策略模式3代理模式4觀察者和發佈訂閱模式5命令模式6享元模式7責任鏈模式8裝飾者模式9狀態模式 ...
  • 各位朋友,本次LZ分享的是狀態模式,在這之前,懇請LZ解釋一下,由於最近公司事情多,比較忙,所以導致更新速度稍微慢了些(哦,往後LZ會越來越忙=。=)。 狀態模式,又稱狀態對象模式(Pattern of Objects for States),狀態模式是對象的行為模式。 狀態模式允許一個對象在其內部 ...
  • 定義: 為其他對象提供一種代理以控制對這個對象的訪問。在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在客戶端和目標對象之間起到中介的作用 角色: 1, 抽象角色:聲明真實對象和代理對象的共同介面。 2, 代理角色:代理對象角色內部含有對真實對象的引用,從而可以操作真實對象,同 ...
  • 為留言板-v1添加登錄功能,使用監聽器檢測會話的變化、維護活躍會話列表 ...
  • XHProf是facebook 開發的一個測試php性能的擴展,本文記錄了在PHP應用中使用XHProf對PHP進行性能優化,查找性能瓶頸的方法。 一、安裝Xhprof擴展 二、修改php.ini 配置中xhprof.output_dir指定了生成的profile文件存儲的位置,我們將其指定為/tm ...
  • Lambda函數又稱匿名函數,匿名函數就是沒有名字的函數,函數沒有名字也行?當然可以啦。有些函數如果只是臨時一用,而且它的業務邏輯也很簡單時,就沒必要非給它取個名字不可。 先來看個簡單lambda函數 x和y是函數的兩個參數,冒號後面的表達式是函數的返回值,你能一眼看出這個函數就是是在求兩個變數的和 ...
  • 一、棧 1.消失的方式不同:方法變數隨著棧方法的釋放而釋放 2.存儲的位置不同,預設複製的處理機制不同:不會給方法的屬性附初值,可以理解為類中的方法中的屬性為局部變數,無法給局部變數附初值,類的狀態由類的成員變數的值來體現,所以稱類是有狀態的對象,而方法中的變數不能預設附初值,則屬於無狀態,而且存儲 ...
  • name = 'mafen mamengmeng' # 首字元大寫 print(name.capitalize()) # 統計指定字元數 print(name.count('m')) # 字元長度 print(len(name)) # 轉換位元組數組,b''bytes 類型 print(type(na... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...