ایجاد توابع کمکی اعتبارسنجی (Creating Validation Helpers)
در این بخش، نحوه ایجاد توابع کمکی اعتبارسنجی (Validation Helper Functions) را بررسی میکنیم. این توابع به ما کمک میکنند تا منطق اعتبارسنجی (Validation Logic) را به صورت مجزا و قابل استفاده مجدد پیادهسازی کنیم.
برای شروع، بیایید یک پکیج اعتبارسنجی (Validation Package) جدید ایجاد کنیم که شامل توابع کمکی (Helper Functions) برای بررسی ورودیهای کاربر (User Inputs) باشد:
$ mkdir internal/validator $ touch internal/validator/validator.go
سپس در این فایل جدید internal/validator/validator.go کد زیر را اضافه کنید:
package validator import ( "slices" "strings" "unicode/utf8" ) // Define a new Validator struct which contains a map of validation error messages // for our form fields. type Validator struct { FieldErrors map[string]string } // Valid() returns true if the FieldErrors map doesn't contain any entries. func (v *Validator) Valid() bool { return len(v.FieldErrors) == 0 } // AddFieldError() adds an error message to the FieldErrors map (so long as no // entry already exists for the given key). func (v *Validator) AddFieldError(key, message string) { // Note: We need to initialize the map first, if it isn't already // initialized. if v.FieldErrors == nil { v.FieldErrors = make(map[string]string) } if _, exists := v.FieldErrors[key]; !exists { v.FieldErrors[key] = message } } // CheckField() adds an error message to the FieldErrors map only if a // validation check is not 'ok'. func (v *Validator) CheckField(ok bool, key, message string) { if !ok { v.AddFieldError(key, message) } } // NotBlank() returns true if a value is not an empty string. func NotBlank(value string) bool { return strings.TrimSpace(value) != "" } // MaxChars() returns true if a value contains no more than n characters. func MaxChars(value string, n int) bool { return utf8.RuneCountInString(value) <= n } // PermittedValue() returns true if a value is in a list of specific permitted // values. func PermittedValue[T comparable](value T, permittedValues ...T) bool { return slices.Contains(permittedValues, value) }
بهطور خلاصه:
در کد بالا، یک نوع ساختار Validator تعریف کردهایم که شامل یک نقشه از پیامهای خطا است. نوع Validator یک متد CheckField() برای افزودن شرطی خطاها به نقشه و یک متد Valid() که بررسی میکند آیا نقشه خطاها خالی است یا نه، ارائه میدهد. همچنین توابع NotBlank()، MaxChars() و PermittedValue() را برای انجام برخی بررسیهای اعتبارسنجی خاص اضافه کردهایم.
مفهومی، این نوع Validator بسیار ساده است، اما این چیز بدی نیست. همانطور که در طول این کتاب خواهیم دید، در عمل بسیار قدرتمند است و به ما انعطافپذیری و کنترل زیادی بر روی بررسیهای اعتبارسنجی و نحوه انجام آنها میدهد.
استفاده از کمککنندهها
خب، بیایید شروع به استفاده از نوع Validator کنیم!
به فایل cmd/web/handlers.go برمیگردیم و آن را بهروزرسانی میکنیم تا یک ساختار Validator را در ساختار snippetCreateForm خود جاسازی کنیم و سپس از این برای انجام بررسیهای اعتبارسنجی لازم بر روی دادههای فرم استفاده کنیم.
به این صورت:
package main import ( "errors" "fmt" "net/http" "strconv" "snippetbox.alexedwards.net/internal/models" "snippetbox.alexedwards.net/internal/validator" // New import ) ... // Remove the explicit FieldErrors struct field and instead embed the Validator // struct. Embedding this means that our snippetCreateForm "inherits" all the // fields and methods of our Validator struct (including the FieldErrors field). type snippetCreateForm struct { Title string Content string Expires int validator.Validator } func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { app.clientError(w, http.StatusBadRequest) return } expires, err := strconv.Atoi(r.PostForm.Get("expires")) if err != nil { app.clientError(w, http.StatusBadRequest) return } form := snippetCreateForm{ Title: r.PostForm.Get("title"), Content: r.PostForm.Get("content"), Expires: expires, // Remove the FieldErrors assignment from here. } // Because the Validator struct is embedded by the snippetCreateForm struct, // we can call CheckField() directly on it to execute our validation checks. // CheckField() will add the provided key and error message to the // FieldErrors map if the check does not evaluate to true. For example, in // the first line here we "check that the form.Title field is not blank". In // the second, we "check that the form.Title field has a maximum character // length of 100" and so on. form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank") form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long") form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank") form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365") // Use the Valid() method to see if any of the checks failed. If they did, // then re-render the template passing in the form in the same way as // before. if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "create.tmpl", data) return } id, err := app.snippets.Insert(form.Title, form.Content, form.Expires) if err != nil { app.serverError(w, r, err) return } http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) }
بنابراین این بهخوبی شکل گرفته است.
اکنون یک بسته internal/validator با قوانین و منطق اعتبارسنجی داریم که میتواند در سراسر برنامه ما استفاده شود و بهراحتی میتوان آن را برای شامل قوانین اضافی در آینده گسترش داد. هم دادههای فرم و هم خطاها بهطور مرتب در یک ساختار snippetCreateForm قرار گرفتهاند — که میتوانیم بهراحتی به قالبهای خود منتقل کنیم — و نحو نمایش پیامهای خطا و پر کردن مجدد دادهها در قالبهای ما ساده و سازگار است.
اگر دوست دارید، برنامه را دوباره اجرا کنید. امیدوارم که فرم و قوانین اعتبارسنجی بهدرستی و به همان شیوه قبلی کار کنند.
اطلاعات اضافی
جنریکها
Go 1.18 اولین نسخه از زبان بود که از جنریکها پشتیبانی میکرد — که با نام فنیتر چندریختی پارامتری نیز شناخته میشود. بهطور کلی، جنریکها به شما اجازه میدهند کدی بنویسید که با انواع مختلف کار کند.
برای مثال، در نسخههای قدیمیتر Go، اگر میخواستید تعداد دفعاتی که یک مقدار خاص در یک برش []string و یک برش []int ظاهر میشود را بشمارید، باید دو تابع جداگانه مینوشتید — یک تابع برای نوع []string و دیگری برای []int. چیزی شبیه به این:
// Count how many times the value v appears in the slice s. func countString(v string, s []string) int { count := 0 for _, vs := range s { if v == vs { count++ } } return count } func countInt(v int, s []int) int { count := 0 for _, vs := range s { if v == vs { count++ } } return count }
اکنون، با جنریکها، میتوان یک تابع count() نوشت که برای []string، []int یا هر برش دیگری از یک نوع قابل مقایسه کار کند. کد به این صورت خواهد بود:
func count[T comparable](v T, s []T) int { count := 0 for _, vs := range s { if v == vs { count++ } } return count }
اگر با نحو کد جنریک در Go آشنا نیستید، اطلاعات زیادی وجود دارد که توضیح میدهد جنریکها چگونه کار میکنند و شما را از طریق نحو نوشتن کد جنریک راهنمایی میکند.
برای بهروز شدن، به شدت توصیه میکنم آموزش رسمی جنریکهای Go را بخوانید و همچنین ۱۵ دقیقه اول این ویدیو را تماشا کنید تا آنچه را که یاد گرفتهاید تقویت کنید.
بهجای تکرار همان اطلاعات در اینجا، میخواهم بهطور مختصر درباره یک موضوع کمتر رایج (اما به همان اندازه مهم!) صحبت کنم: چه زمانی از جنریکها استفاده کنیم.
فعلاً، باید سعی کنید از جنریکها با احتیاط و دقت استفاده کنید.
میدانم که ممکن است کمی خستهکننده به نظر برسد، اما جنریکها یک ویژگی نسبتاً جدید زبان هستند و بهترین روشها برای نوشتن کد جنریک هنوز در حال شکلگیری هستند. اگر در یک تیم کار میکنید یا کد را بهصورت عمومی مینویسید، همچنین به خاطر داشته باشید که همه توسعهدهندگان Go دیگر لزوماً با نحوه کار کد جنریک آشنا نیستند.
شما نیاز به استفاده از جنریکها ندارید و استفاده نکردن از آنها اشکالی ندارد.
اما حتی با این هشدارها، نوشتن کد جنریک میتواند در برخی سناریوها واقعاً مفید باشد. بهطور کلی، ممکن است بخواهید آن را در نظر بگیرید:
- اگر خود را در حال نوشتن کد تکراری برای انواع دادههای مختلف میبینید. مثالهایی از این ممکن است عملیاتهای رایج بر روی برشها، نقشهها یا کانالها باشد — یا کمککنندههایی برای انجام بررسیهای اعتبارسنجی یا اظهارات تست بر روی انواع دادههای مختلف.
- وقتی در حال نوشتن کد هستید و خود را در حال استفاده از نوع
any(رابط خالیinterface{}) میبینید. مثالی از این ممکن است زمانی باشد که در حال ایجاد یک ساختار داده (مانند صف، کش یا لیست پیوندی) هستید که نیاز به کار با انواع مختلف دارد.
در مقابل، احتمالاً نمیخواهید از جنریکها استفاده کنید:
- اگر کد شما را سختتر برای درک یا کمتر واضح میکند.
- اگر همه انواعی که نیاز دارید با آنها کار کنید مجموعهای از متدهای مشترک دارند — در این صورت بهتر است یک نوع
interfaceمعمولی تعریف و استفاده کنید. - فقط به این دلیل که میتوانید. ترجیحاً بهطور پیشفرض کد غیرجنریک بنویسید و بعداً به نسخه جنریک تغییر دهید فقط اگر واقعاً نیاز باشد.
واژهنامه اصطلاحات فنی
| اصطلاح فارسی | معادل انگلیسی | توضیح |
|---|---|---|
| توابع کمکی اعتبارسنجی | Validation Helper Functions | توابعی برای کمک به اعتبارسنجی دادهها |
| منطق اعتبارسنجی | Validation Logic | قوانین و شرایط اعتبارسنجی دادهها |
| پکیج اعتبارسنجی | Validation Package | مجموعهای از توابع اعتبارسنجی |
| توابع کمکی | Helper Functions | توابع کمکی برای سادهسازی عملیات |
| ورودیهای کاربر | User Inputs | دادههای وارد شده توسط کاربر |
| قوانین اعتبارسنجی | Validation Rules | شرایط لازم برای معتبر بودن دادهها |
| خطاهای اعتبارسنجی | Validation Errors | پیامهای خطا در مورد دادههای نامعتبر |
| کد قابل استفاده مجدد | Reusable Code | کدی که میتواند در چند جا استفاده شود |
| بررسی صحت | Validation Check | فرآیند بررسی معتبر بودن داده |
| پیامهای خطا | Error Messages | پیامهای توضیح دهنده خطاها |