golang validation function pattern

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.

 1package config
 2
 3type Config struct {
 4    ConfigA string        `json:"config-a"`
 5    Timeout time.Duration `json:"config-b"`
 6}
 7
 8type ValidateFunc func(*Config) error
 9
10var ValidateConfigA ValidateFunc = func(c *Config) error {
11    if strings.HasPrefix(c.ConfigA, "foo-") {
12        return nil
13    }
14
15    return fmt.Errorf("ConfigA should have prefix 'foo-'")
16}
17
18var ValidateTimeout ValidateFunc = func(c *Config) error {
19    if c.Timeout > 0 {
20        return nil
21    }
22
23    return fmt.Errorf("Timeout should be > 0")
24}
25
26func (c *Config) Validate(funcs ...ValidateFunc) (errs []error) {
27    for _, fn := range funcs {
28        err := fn(c)
29        if err != nil {
30            errs = append(errors, err)
31        }
32    }
33
34    return errs
35}

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

 1package client
 2
 3func NewConfig() *config.Config {
 4    c := &config.Config{
 5        ConfigA: "whatever",
 6        Timeout: 10,
 7    }
 8
 9    errs := c.Validate(
10        config.ValidateConfigA,
11        config.ValidateTimeout,
12    )
13
14}
15
16

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

 1
 2package client
 3
 4func NewConfig() *config.Config {
 5    c := &config.Config{
 6        ConfigA: "whatever",
 7        Timeout: 10,
 8    }
 9
10    errs := c.Validate(
11        config.ValidateConfigA,
12    )
13}

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

 1package config
 2
 3.
 4.
 5.
 6
 7type ValidateFunc func(*Config) error
 8
 9func ValidateEqualTimeout(timeout time.Duration) func(*Config) error {
10    return func(c *Config) error {
11        if c.Timeout == timeout {
12            return nil
13        }
14
15        return fmt.Errorf("expected timeout: %ss, got: %ss", timeout.Seconds(), c.Timeout.Seconds())
16    }
17}
18
19.
20.
21.
22

and then on client side:

 1
 2package client
 3
 4func NewConfig() *config.Config {
 5    c := &config.Config{
 6        ConfigA: "whatever",
 7        Timeout: 10,
 8    }
 9
10    errs := c.Validate(
11        config.ValidateConfigA,
12        config.ValidateEqualTimeout(20 * time.Seconds),
13    )
14}
15

hope it was useful