Introducing GoMethods

GoMethods is a linter that’ll help Go developers to keep non-reflection, holistic methods like serializer, validator and comparators up-to-date and complete, even you miss to update them following 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
}

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 golangci plugin. I call it GoMethods. It is a linter which warns you when a doc-comment-annotated 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:

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

As well as checking all fields, GoMethods can also be directed to only require exported fields:

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

Install GoMethods as a standalone linter and invoke from shell as:

go install go.ufukty.com/gomethods@latest

Example

Briefly:

type Node struct {
  Exported   any
  unexported any
}

// gomethods: all
func (n Node) Foo() {}

// gomethods: exported
func (n Node) Bar() {}

Run the linter:

$ gomethods ./...
.../example.go:8:1: missing fields: Exported, unexported
.../example.go:11:1: missing field: Exported

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

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