Skip to main content
  1. Projects/

Go Validate

··539 words·3 mins

While working with golang in my various projects, I realized that there were a lack of validation tools for golang. I decided to make a validation function in golang from scratch to learn more about golang and to make a tool that I can use in my future projects.

Introduction #

What is validation? #

Validation is the process of checking if the input is valid. For example, if we want to check if a user’s email is valid, we can check if the email contains an @ symbol. If it does not, we can say that the email is invalid. (Please do not use this in your projects as it is not a good way to check if an email is valid).

Goals of the project #

The goals of the project are as follows:

  • Make validation syntax easier to read.
  • Able to support compounding of different validation functions.
  • Able to insert custom error messages.

Architecture #

--- title: Go-Validator --- classDiagram Validator <|-- LazyValidator Validator <|-- EagerValidator Validator <|-- ParallelValidator Validator : +Validate() error Validator : +WithOptions(...Validate) *T Require <|-- Validator Require : func(Test)bool Validate <|-- Require Validate : func()error class Validate{ +WithError(err error) Validate +Not(err error) Validate } class LazyValidator{ +WithOptions(...Validate) *T +Validate() error } class EagerValidator{ +WithOptions(...Validate) *T +Validate() error } class ParallelValidator{ +WithOptions(...Validate) *T +Validate() error }

Installing the package #

Prerequisites #

  • Go 1.16 or higher

Install command #

To install the package, you can use the following command:

go get github.com/Jh123x/go-validate

Demo #

Imagine I am working on a test suite for my server. I want to see if it is working correctly. Here are the constraints for the response.

type Response struct {
	Code        int            // Must be non-zero.
	Message     string         // Must be non-empty.
	Extras      map[string]any // Must be non-nil.
	Optional    string         // Optional
	SetIfOptSet string         // Set if Optional is set, empty otherwise.
}

Normally, if I want to check it, I will have to do something like this:

func validate(resp Response) error {
    if resp.Code == 0 {
        return fmt.Errorf("Code cannot be 0")
    }

    if len(resp.Message) == 0 {
        return fmt.Errorf("Message cannot be empty")
    }

    if resp.Extras == nil {
        return fmt.Errorf("Extras cannot be nil")
    }

    if len(resp.Optional) > 0 && len(resp.SetIfOptSet) == 0 {
        return fmt.Errorf("Optional cannot be empty")
    }

    return nil
}

Notice that if there were more conditions, I will have to add more of the if statements to check for them. This will make the code very long and hard to read.

By using this wrapper, I can do the following instead.

func validate(resp Response) error {
	return NewValidator().WithOptions(
		options.IsNotEmpty(resp.Code),
		options.IsNotEmpty(resp.Message),
		options.WithRequire(func() bool { return resp.Extras != nil }, errTest),
		options.Or(
			options.And(
				options.IsEmpty(resp.Optional),
				options.IsEmpty(resp.SetIfOptSet),
			),
			options.And(
				options.IsNotEmpty(resp.Optional),
				options.IsNotEmpty(resp.SetIfOptSet),
			),
		),
	).Validate()
}

The results at each step is also cached. At each step of the validation we can save the result to a variable and use it later.

cond1 = NewValidator().WithOptions(
    options.IsNotEmpty(resp.Code),
    options.IsNotEmpty(resp.Message),
)

//EG: Check optional is non-empty if not empty
split1 = cond1.WithOptions(
    options.IsNotEmpty(resp.Optional),
).validate()

//EG: Check if is empty in this case
split2 = cond1.WithOptions(
    options.IsEmpty(resp.Optional),
).validate()

Trade-offs #

What we lose:

  • 0.56 to 0.84 times slower than the raw if statements

What we gain:

  • Easier to read code
  • Easier to add new conditions