こんにちは。KOUKIです。
今回は、サーバーサイド側のバリデーション処理をGo言語で実装しようと思います。
バリデーション処理とは、Webページのフォームなどから送信されたデータが「開発者が意図したデータ通りの形式で送られてきたか」チェックする仕組みです。
Web開発をする上では欠かせない、非常に重要な事柄なので、この機会に一緒に学習しましょう^^
<目次>
ワークスペースの用意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
mkdir validation-sample cd validation-sample/ go mod init validation touch main.go mkdir -p internal/forms touch internal/forms/forms.go touch internal/forms/errors.go mkdir domain touch domain/models.go mkdir templates touch templates/form.tmpl $ tree validation-sample ├── domain │ └── models.go ├── go.mod ├── internal │ └── forms │ ├── errors.go │ └── forms.go ├── main.go └── templates ├── form.tmpl └── result.tmpl |
ざっくりとWebページを作る
Go言語では、text/templateパッケージを使うとページ情報をブラウザ上に表示させることができます。
この機能を使って、まずはWebページからフォームデータを送信できる簡易アプリを作成しましょう。
models.go
models.goには、送信データを格納する構造体を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 |
// models.go package domain type FormModel struct { Email string `json:"email"` Password string `json:"password"` } type TemplateData struct { Form map[string]interface{} Data map[string]interface{} } |
main.go
main.goには、Webサーバーとテンプレートファイルをブラウザへ返す処理を実装します。バリデーションの呼び出しもこのファイルに書くつもりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
// main.go package main import ( "fmt" "log" "net/http" "text/template" "validation/domain" ) const portNumber = ":8080" func Home(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { // 初期値は空 var emptyFormModel domain.FormModel data := make(map[string]interface{}) data["validation"] = emptyFormModel renderTemplate(w, r, "form.tmpl", &domain.TemplateData{ Form: nil, Data: data, }) } else if r.Method == "POST" { err := r.ParseForm() if err != nil { log.Fatal(err) return } // Get関数でデータを取得できる // emailやパスワードはHTMLのinputのname validation := domain.FormModel{ Email: r.Form.Get("email"), Password: r.Form.Get("password"), } fmt.Println("Get Form Value: ", validation) // バリデーションチェック(暫定) check := true if !check { data := make(map[string]interface{}) data["validation"] = validation renderTemplate(w, r, "form.tmpl", &domain.TemplateData{ Form: nil, Data: data, }) } renderTemplate(w, r, "result.tmpl", &domain.TemplateData{}) } } func main() { http.HandleFunc("/", Home) fmt.Println(fmt.Printf("Starting application on port %s", portNumber)) log.Fatal(http.ListenAndServe(portNumber, nil)) } // templateを返すメソッド func renderTemplate( w http.ResponseWriter, r *http.Request, tmpl string, data *domain.TemplateData) { parsedTemplate, _ := template.ParseFiles("./templates/" + tmpl) // dataを格納 err := parsedTemplate.Execute(w, data) if err != nil { fmt.Println("Error parsing template:", err) return } } |
などで指定している引数は、input要素のname属性の値です。ここはわかりずらいので、明記しておきます。r.Form.Get("email")
1 2 3 4 5 6 7 8 9 10 |
<input class="form-control {{with .Form.Errors.Get "email"}} is-invalid {{end}}" type="text" name="email" <<<<<<<<<<< ここ autocomplete="off" value="{{$res.Email}}" required > |
form.tmpl
ここには、Webページのフォームを記載します。
「{{$res := index .Data “validation”}}」などの構文は、text/templateパッケージのテンプレート構文です。本記事の目的とは関係ないので、参考程度にしておいてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
<!-- form.tmpl --> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>My Form Validation</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> <style> body { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; } </style> </head> <body> <div class="container"> <h1>Validation Check</h1> {{$res := index .Data "validation"}} <form method="post" novalidate> <div class="mb-3"> <label class="form-label">Email address</label> {{with .Form.Errors.Get "email"}} <label class="text-danger">{{.}}</label> {{end}} <input class="form-control {{with .Form.Errors.Get "email"}} is-invalid {{end}}" type="text" name="email" autocomplete="off" value="{{$res.Email}}" required > </div> <div class="mb-3"> <label class="form-label">Password</label> {{with .Form.Errors.Get "password"}} <label class="text-danger">{{.}}</label> {{end}} <input class="form-control {{with .Form.Errors.Get "password"}} is-invalid {{end}}" type="password" name="password" value="{{$res.Password}}" required > </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </body> </html> |
result.tmpl
result.tmplは、バリデーションチェックにpassした時の遷移先です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!doctype html> <html lang="en"> <head> <title>My Form Validation</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> </head> <body> <div class="container"> <h1>Result</h1> <h2>Validation OK</h2> </div> </body> </html> |
アプリを起動
アプリを起動してみましょう。
1 2 |
$ go run main.go Starting application on port :808034 <nil> |
次に「http://localhost:8080/」へアクセスします。

適当に文字を打ち込んで、Submitしてみます。

こんな感じになりました。
バリデーション処理を実装し、そのチェックがNGの場合はResultページへは遷移せず、フォームのテキストボックスにエラーを表示するようにします。
バリデーション処理の実装
バリデーション処理を実装する上で一つ言っておきたいことがあります。
それは、やり方は色々あるということです。
ここで紹介する方法は、その内の一つになります。
errors.go
errors.goには、バリデーションチェックのエラー情報を格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// errors.go package forms type errors map[string][]string /* エラーを格納する fieldはform inputのname属性の値 messageはエラーメッセージ */ func (e errors) Add(field, message string) { e[field] = append(e[field], message) } // エラーを取得 func (e errors) Get(field string) string { es := e[field] if len(es) == 0 { return "" } // 複数格納されている可能性があるので、そのうち一つだけ返す return es[0] } |
errorsには、バリデーションチェック結果でNGの場合、フォームのフィールド名(email,password)とエラーメッセージを格納します。
エラーメッセージは複数格納される場合があるので、最初にNGになったエラーのみを返すようにしています。
forms.go
forms.goには、カスタムフォーム構造体とバリデーションメソッドを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// forms.go package forms import ( "net/url" "strings" ) type Form struct { url.Values Errors errors } // コンストラクタ func New(data url.Values) *Form { return &Form{ data, // errors.goに設定したerrors errors(map[string][]string{}), } } // エラーの存在チェック func (f *Form) Valid() bool { return len(f.Errors) == 0 } |
バリデーションチェックメソッドは、後ほど実装します。
url.Valuesは、フォームからデータを送られてきた時、PostForm関数で取得できる戻り値の型です。パースされたフォームの情報が入り、引数にはinput要素のname(email,password)がkeyとして入るようです。
公式サイトにわかりやすい例があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import ( "fmt" "net/url" ) func main() { v := url.Values{} v.Set("name", "Ava") // nameがinputのname v.Add("friend", "Jess") // friendがinputのname v.Add("friend", "Sarah") v.Add("friend", "Zoe") // v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe" fmt.Println(v.Get("name")) fmt.Println(v.Get("friend")) fmt.Println(v["friend"]) } |
1 2 3 |
Ava Jess [Jess Sarah Zoe] |
フォームを作成したので、models.goのTemplateDataの「Form」を書き換えましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// models.go package domain import "validation/internal/forms" type FormModel struct { Email string `json:"email"` Password string `json:"password"` } type TemplateData struct { Form *forms.Form // ここを書き換える Data map[string]interface{} } |
必須チェック
最初のバリデーションとして、必須チェックをforms.goに追加してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// forms.go ... /* 必須チェック ...でいくらでもパラメータを受け付けられる */ func (f *Form) Required(fields ...string) { for _, field := range fields { // url.Valuesオブジェクトを持っているので、 // Getメソッドが使える value := f.Get(field) if strings.TrimSpace(value) == "" { // エラーを格納 f.Errors.Add(field, "この項目は必須です") } } } |
fieldsには、emailやpasswordなどフォームのname属性の値が入ります。
main.goのPOST処理に、このバリデーションメソッドを組み込みます。※GET処理でrenderTemplateのFormの引数も変えてます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// main.go func Home(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { var emptyFormModel domain.FormModel data := make(map[string]interface{}) data["validation"] = emptyFormModel renderTemplate(w, r, "form.tmpl", &domain.TemplateData{ Form: forms.New(nil), // 追加 Data: data, }) return } else if r.Method == "POST" { err := r.ParseForm() if err != nil { log.Fatal(err) } validation := domain.FormModel{ Email: r.Form.Get("email"), Password: r.Form.Get("password"), } fmt.Println("Get Form Value: ", validation) // 追加 // PostFormの戻り値はurl.url.Values // Postで送信されたデータをパースしている form := forms.New(r.PostForm) // formのinput要素のnameに指定した情報を送る form.Required("email", "password") // エラーかチェック if !form.Valid() { data := make(map[string]interface{}) data["validation"] = validation renderTemplate(w, r, "form.tmpl", &domain.TemplateData{ Form: form, Data: data, }) return } else { renderTemplate(w, r, "result.tmpl", &domain.TemplateData{}) return } } } |
アプリを再起動します。
1 2 |
Ctrl + c go run main.go |
http://localhost:8080にアクセスし、特に何も入力しないでSubmitボタンを押してみましょう。

いい感じですね。テキストボックスの赤いスタイリングは、Bootstrap5のis-invalidを使って、装飾してます。
文字数チェック
必須チェックと同じ要領で、文字数チェックを実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// forms.go // 最小文字数チェック func (f *Form) MinLength(field string, length int, r *http.Request) bool { x := r.Form.Get(field) if len(x) < length { f.Errors.Add(field, fmt.Sprintf( "このフィールドの最小文字数は%d文字です", length)) return false } return true } // 最大文字数チェック func (f *Form) MaxLength(field string, length int, r *http.Request) bool { x := r.Form.Get(field) if len(x) > length { f.Errors.Add(field, fmt.Sprintf( "このフィールドの最大文字数は%d文字です", length)) return false } return true } |
先ほどと同じように、main.goのHome関数のPOST処理からこれらのメソッドを呼び出しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// main.go func Home(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { ... } else if r.Method == "POST" { ... fmt.Println("Get Form Value: ", validation) form := forms.New(r.PostForm) form.Required("email", "password") // emailの最小文字数は10 form.MinLength("email", 10, r) // passwordの最大文字数は10 form.MaxLength("password", 10, r) if !form.Valid() { ... }) return } else { ... } } } |
アプリを起動し直して、動作検証をします。
Email: test@com
Password: kkkkkkkkkkkkkkkkkkkkkkkkkkkkk

OKですね。機能追加がかなり楽です。
Emailチェック
Emailが正しい形式で送信されてきたかチェックしてみましょう。
仕事で、asaskevich/govalidatorを使ったことがあるので、これを組み込んでみます。
1 2 |
# モジュールをインストール go get github.com/asaskevich/govalidator |
forms.goに以下のメソッドを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// forms.go package forms import ( "fmt" "net/http" "net/url" "strings" "github.com/asaskevich/govalidator" ) ... // Emailチェック func (f *Form) IsEmail(field string) { if !govalidator.IsEmail(f.Get(field)) { f.Errors.Add(field, "無効なEmailアドレスです") } } |
簡単ですね。組み合わせるのも悪くないです。
main.goのHome関数を編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func Home(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { ... } else if r.Method == "POST" { ... fmt.Println("Get Form Value: ", validation) form := forms.New(r.PostForm) form.Required("email", "password") form.MinLength("email", 10, r) form.MaxLength("password", 10, r) form.IsEmail("email") // 追加 if !form.Valid() { ... return } else { ... } } } |
アプリを再起動して、フォームから次のパラメータを送信します。
Email: testdusuyo
Password: password

OKですね。
おわりに
めっちゃいい感じですね!
機能追加も楽だし、テストもしやすそうです。
バリデーションは、サーバーサイドとフロントエンド両方で実装することが望ましいと思いますが、結構めんどくさいところなので、こんな感じでスマートに実装できると良さそうですね^^
それでは、また!
関連記事
Go記事まとめ
Goソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
// main.go package main import ( "fmt" "log" "net/http" "text/template" "validation/domain" "validation/internal/forms" ) const portNumber = ":8080" func Home(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { var emptyFormModel domain.FormModel data := make(map[string]interface{}) data["validation"] = emptyFormModel renderTemplate(w, r, "form.tmpl", &domain.TemplateData{ Form: forms.New(nil), Data: data, }) return } else if r.Method == "POST" { err := r.ParseForm() if err != nil { log.Fatal(err) } validation := domain.FormModel{ Email: r.Form.Get("email"), Password: r.Form.Get("password"), } fmt.Println("Get Form Value: ", validation) form := forms.New(r.PostForm) form.Required("email", "password") form.MinLength("email", 10, r) form.MaxLength("password", 10, r) form.IsEmail("email") if !form.Valid() { data := make(map[string]interface{}) data["validation"] = validation renderTemplate(w, r, "form.tmpl", &domain.TemplateData{ Form: form, Data: data, }) return } else { renderTemplate(w, r, "result.tmpl", &domain.TemplateData{}) return } } } // templateを返すメソッド func renderTemplate( w http.ResponseWriter, r *http.Request, tmpl string, data *domain.TemplateData) { parsedTemplate, _ := template.ParseFiles("./templates/" + tmpl) // dataを格納 err := parsedTemplate.Execute(w, data) if err != nil { fmt.Println("Error parsing template:", err) return } } func main() { http.HandleFunc("/", Home) fmt.Println(fmt.Printf("Starting application on port %s", portNumber)) log.Fatal(http.ListenAndServe(portNumber, nil)) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// models.go package domain import "validation/internal/forms" type FormModel struct { Email string `json:"email"` Password string `json:"password"` } type TemplateData struct { Form *forms.Form Data map[string]interface{} } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// errors.go package forms type errors map[string][]string /* エラーを格納する fieldはform inputのname属性の値 messageはエラーメッセージ */ func (e errors) Add(field, message string) { e[field] = append(e[field], message) } // エラーを取得 func (e errors) Get(field string) string { es := e[field] if len(es) == 0 { return "" } // 複数格納されている可能性があるので、そのうち一つだけ返す return es[0] } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
// forms.go package forms import ( "fmt" "net/http" "net/url" "strings" "github.com/asaskevich/govalidator" ) type Form struct { url.Values Errors errors } // コンストラクタ func New(data url.Values) *Form { return &Form{ data, // errors.goに設定したerrors errors(map[string][]string{}), } } // エラーの存在チェック func (f *Form) Valid() bool { return len(f.Errors) == 0 } /* 必須チェック ...でいくらでもパラメータを受け付けられる */ func (f *Form) Required(fields ...string) { for _, field := range fields { // url.Valuesオブジェクトを持っているので、 // Getメソッドが使える value := f.Get(field) if strings.TrimSpace(value) == "" { // エラーを格納 f.Errors.Add(field, "この項目は必須です") } } } // 最小文字数チェック func (f *Form) MinLength(field string, length int, r *http.Request) bool { x := r.Form.Get(field) if len(x) < length { f.Errors.Add(field, fmt.Sprintf( "このフィールドの最小文字数は%d文字です", length)) return false } return true } // 最大文字数チェック func (f *Form) MaxLength(field string, length int, r *http.Request) bool { x := r.Form.Get(field) if len(x) > length { f.Errors.Add(field, fmt.Sprintf( "このフィールドの最大文字数は%d文字です", length)) return false } return true } // Emailチェック func (f *Form) IsEmail(field string) { if !govalidator.IsEmail(f.Get(field)) { f.Errors.Add(field, "無効なEmailアドレスです") } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
<!-- form.tmpl --> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>My Form Validation</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> <style> body { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; } </style> </head> <body> <div class="container"> <h1>Validation Check</h1> {{$res := index .Data "validation"}} <form method="post" novalidate> <div class="mb-3"> <label class="form-label">Email address</label> {{with .Form.Errors.Get "email"}} <label class="text-danger">{{.}}</label> {{end}} <input class="form-control {{with .Form.Errors.Get "email"}} is-invalid {{end}}" type="text" name="email" autocomplete="off" value="{{$res.Email}}" required > </div> <div class="mb-3"> <label class="form-label">Password</label> {{with .Form.Errors.Get "password"}} <label class="text-danger">{{.}}</label> {{end}} <input class="form-control {{with .Form.Errors.Get "password"}} is-invalid {{end}}" type="password" name="password" value="{{$res.Password}}" required > </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!doctype html> <html lang="en"> <head> <title>My Form Validation</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> </head> <body> <div class="container"> <h1>Result</h1> <h2>Validation OK</h2> </div> </body> </html> |
コメントを残す
コメントを投稿するにはログインしてください。