🚀

Goの次世代バリデーションツールgovalid

に公開

みなさん普段から開発しているサービスのバリデーションはしっかりしていますか?

Goでバリデーションをする際には2つパターンがあると思います。

  • 標準ライブラリや自分たちでバリデーションするコードを手書きする
  • ライブラリと組み合わせてバリデーションを行う

このパターンが多くの人が取る選択肢でありますし、自分で書くよりもできるだけ楽をしたくてライブラリに頼ったりするケースが多いのではないでしょうか?

既存バリデーションの問題点

例えば手書きで書くことで自分たちで正しく境界値を定めたり依存ライブラリの削減、加えてオーバーヘッドを減らすといったメリットがあるでしょう。しかし全てのリクエストに対して書いていくのは大変ですし、AIエージェントに書かせるにはトークン数的にも富豪でなければ削減していきたいところです。

ライブラリで考えると無難かつ有名どころでいくとgo-playground/validatorでしょう。
しかしこのライブラリはstructタグで制御することからも分かるとおりreflectパッケージを用いて動的に検証しています。そのためリクエスト数が多いサービスではこれだけでメモリアロケーションなどが多量に発生します。またstructがネストしたようなものをバリデーションしていく場合、全てのフィールドをreflectを使って解析するため、いくら富豪的にメモリなどが使える現代であっても可能な限り避けたいですし、そもそもコンパイル可能な言語でこのような選択肢を取るのは個人的にはかなり微妙だと思います。
またvalidationの結果がboolで返ってくるためどのエラーで失敗したかわからない、errors.Isなどと組み合わせることができないのも個人的には好ましくありませんでした。

govalid

そこで作成したのが今回紹介するgovalidというツールです

https://github.com/sivchari/govalid

このツールはkubebuilderのようにマーカーコメントから解析して事前にそれぞれのstructへのバリデーションコードを全て自動生成するツールです。

govalidでは下記のようにマーカーコメントを用いてどのようなバリデーションを期待するかを制御することができます。

//go:generate govalid .

type User struct {
	// +govalid:required
	Name string `json:"name"`
	
	// +govalid:required
	// +govalid:email
	Email string `json:"email"`
	
	// +govalid:gte=0
	// +govalid:lte=120
	Age int `json:"age"`
}

例えばこのコードに対して実行すると下記のようなコードが自動生成されます。

// Code generated by govalid; DO NOT EDIT.

package main

import (
	"errors"
	"github.com/sivchari/govalid/validation/validationhelper"
)

var (
	ErrNilUser                     = errors.New("input User is nil")
	ErrUserNameRequiredValidation  = errors.New("field Name is required")
	ErrUserEmailRequiredValidation = errors.New("field Email is required")
	ErrUserEmailEmailValidation    = errors.New("field Email must be a valid email address")
	ErrUserAgeGTEValidation        = errors.New("field Age must be greater than or equal to 0")
	ErrUserAgeLTEValidation        = errors.New("field Age must be less than or equal to 120")
)

func ValidateUser(t *User) error {
	if t == nil {
		return ErrNilUser
	}

	if len(t.Name) == 0 {
		return ErrUserNameRequiredValidation
	}

	if len(t.Email) == 0 {
		return ErrUserEmailRequiredValidation
	}

	if !validationhelper.IsValidEmail(t.Email) {
		return ErrUserEmailEmailValidation
	}

	if !(t.Age >= 0) {
		return ErrUserAgeGTEValidation
	}

	if !(t.Age <= 120) {
		return ErrUserAgeLTEValidation
	}

	return nil
}

あとは実際に任意のところでこのValidateUserを使用することでバリデーションが実行されます。

既存ライブラリとの比較

圧倒的なパフォーマンス向上

こちらがベンチマーク結果になります。

今回は一番スター数の多いgo-playground/validatorとの比較結果を載せます。

置き換えるだけで最低でも約5倍のパフォーマンス改善がなされます。必須であることを表すRequiredを挙げて強調すると約45倍のパフォーマンスが出ています。

実装を開始してとんでもないパフォーマンスが出て思わずポストしてしまったのを覚えています。
https://x.com/sivchari/status/1937455270134972870

errorとしてバリデーションを扱える

バリデーション結果がエラー型として返るためerrors.Isなどを用いてハンドリングすることが可能です。

まとめ

今回は最近作成を始めたgovalidというツールをご紹介しました。
バリデーションコードの自動生成に加えて置き換えるだけでパフォーマンスが良くなるという特徴を持ったツールです。

個人的にはgovalidを使うことでサービスのパフォーマンスをかなり改善できることからもGoのバリデーションライブラリの定番にしていきたいと思っています。
少しでもいいと思ったらスターや拡散をしていただけると非常に嬉しいです。

またまだまだやりたいことやバグもあるかもしれないためぜひフィードバックをいただけると嬉しいです。

ここまで読んでいただきありがとうございました!

Discussion