This tutorial walk you through using a validation function pattern to allow users to choose validations they want to perform

More than often we find us into implementing validations for structs defined in golang. Sometimes a common validation of struct fields is enough, but sometimes we need to provide users the flexibility of choosing their validations.

One way to do that would be to ask users to implement their validations on their side, and not use validation provided by the struct. But this means a lot of code duplications if there are multiple clients within the same codebase.

Another pattern which can be used is to provide a validation func pattern.

e.g.

package config

type Config struct {
    ConfigA string        `json:"config-a"`
    Timeout time.Duration `json:"config-b"`
}

type ValidateFunc func(*Config) error

var ValidateConfigA ValidateFunc = func(c *Config) error {
    if strings.HasPrefix(c.ConfigA, "foo-") {
        return nil
    }

    return fmt.Errorf("ConfigA should have prefix 'foo-'")
}

var ValidateTimeout ValidateFunc = func(c *Config) error {
    if c.Timeout > 0 {
        return nil
    }

    return fmt.Errorf("Timeout should be > 0")
}

func (c *Config) Validate(funcs ...ValidateFunc) (errs []error) {
    for _, fn := range funcs {
        err := fn(c)
        if err != nil {
            errs = append(errors, err)
        }
    }

    return errs
}

and now client who need to do validations for both configs can do following:

package client

func NewConfig() *config.Config {
    c := &config.Config{
        ConfigA: "whatever",
        Timeout: 10,
    }

    errs := c.Validate(
        config.ValidateConfigA,
        config.ValidateTimeout,
    )

}

and client who need to validate only ConfigA can do following:


package client

func NewConfig() *config.Config {
    c := &config.Config{
        ConfigA: "whatever",
        Timeout: 10,
    }

    errs := c.Validate(
        config.ValidateConfigA,
    )
}

Also if you want to add your own custom validation, the package config can expose a function like follows:

package config

.
.
.

type ValidateFunc func(*Config) error

func ValidateEqualTimeout(timeout time.Duration) func(*Config) error {
    return func(c *Config) error {
        if c.Timeout == timeout {
            return nil
        }

        return fmt.Errorf("expected timeout: %ss, got: %ss", timeout.Seconds(), c.Timeout.Seconds())
    }
}

.
.
.

and then on client side:


package client

func NewConfig() *config.Config {
    c := &config.Config{
        ConfigA: "whatever",
        Timeout: 10,
    }

    errs := c.Validate(
        config.ValidateConfigA,
        config.ValidateEqualTimeout(20 * time.Seconds),
    )
}

hope it was useful