Introducing Golistics

Golistics is a linter that’ll help Go developers to keep type-safe and reflectionless “holistic” methods like serializer, validator and comparators up-to-date and complete, even when developers miss to update after changing the field list.

Problem

Let me introduce you the problem.

Instead of using reflection, I usually implement manual methods that needs to visit all fields of a struct. Like validators, serializers or comparators. Eg:

type Request struct {
  Age    HumanAge
  School School
  Class  StudentClass
}

func (r Request) Validate() map[string]error {
  errs := map[string]error{}
  if err := r.Age.Validate(); err != nil {
    errs["age"] = err
  }
  // ...
}

func (r Request) String() string {
  return fmt.Sprintf(`Age: %q, School: %q, Class: %q`, r.Age, r.School, r.Class)
}

func (r Request) Equals(s Request) bool {
  return r.Age == s.Age &&
    r.School == s.School &&
    r.Class == s.Class
}

Note that the Request.String and Request.Equals is accessing all the fields by principle. Which means they also need to preserve that propery throughout the future changes made in the field list of Request type.

The problem is that each such struct adds ~3 new long-term liability to my chore list because each such method needs to be updated with the list of fields of the struct. Adding the 4th field to Request creates a need navigate to the struct’s methods and check if anyone needs update.

I thought why it would not be better to use linter to perform those checks at CI if any such method needs an update?

Solution

Now let me introduce you my solution.

I created a program I call Golistics, honoring the use of word “holistic” as if it is a known term to describe the methods that supposed to access all fields of a struct, or they get outdated.

Golistics is a Go-vet style analyzer (or a linter-plugin in a broad sense) which warns the developer when a desired method goes out-of-sync with the receiver’s (or underlying type’s) field list. You opt-in any method for such checks by adding this annotation to the method doc-comment:

//golistics:all
func (r Request) String() string

Note, method doc-comment is the comment group above the method’s signature.

Partial holisticallity is also supported. As well as checking all fields, Golistics can also be directed to only require exported fields:

//golistics:exported
func (r Request) String() string

Install

Installing Golistics is easy. See the repository for up-to-date instructions.

Example

Briefly:

type (
  Borders struct {
    Top, Right, Bottom, Left Border
  }
  Margin struct {
    Top, Right, Bottom, Left any
  }
  Dimensions struct {
    Height     any
    Width      any
    unexported any
  }
)

//golistics:exported
func (s Dimensions) Strings() []string {
  return nil
}

//golistics:all
func (s Borders) IsEqual(y Borders) bool {
  return false
}

//golistics:all
func (s Margin) IsEqual(y Margin) bool {
  return safeEq(s.Right, y.Right) &&
    safeEq(s.Bottom, y.Bottom) &&
    safeEq(s.Left, y.Left)
}

Run the linter:

$ golistics ./...
gss.go:173:1: missing fields: Height, Width
gss.go:241:1: missing fields: Bottom, Left, Right, Top
gss.go:246:1: missing field: Top

Testing

The solution is both unit and manually tested.

The test case for unit test can be seen on GitHub with // want:-like assertions. Maybe it communicates the problem better than the short example above.

Summary

Golistics reduces the duration for noticing outdated holistic methods and possbility to ship code with them; thus improving reliability and maintainability.