Go is great for building APIs. It’s common for APIs to perform some validation on the data structures used in request bodies.
some structure
Assume we’re handling a request with the following body:
{
"thing_id": 123,
"foo": "fizz",
"bar": "buzz",
"some_date": "2015-08-09T07:25:43-05:00"
}
This can be unmarshalled onto the following Go struct:
type SomeRequest struct {
ThingID int64 `json:"thing_id"`
Foo, Bar string
SomeDate time.Time `json:"some_date"`
}
package json
will do the heavy lifting to parse the date and convert the JSON numeric into an int64
. If the client had supplied something other than an ISO8601 date string for some_date
, package json
would squawk. The same goes for values in thing_id
that cannot be converted to an int64
.
validate me
Now let’s validate that the client supplied data for each attribute in the struct.
There are some great packages available to define constraints with struct annotations. However, I prefer hand-rolling validation with a few simple funcs.
func (req SomeRequest) validate() error {
if req.Foo == "" {
return errInvalidRequest
}
if req.Bar == "" {
return errInvalidRequest
}
if req.ThingID == 0 {
return errInvalidRequest
}
if req.SomeDate.UnixNano() == 0 {
return errInvalidRequest
}
return nil
}
This naïve approach gets the job done, but we can do better. gocyclo calculates the cyclomatic complexity of the func above at 5.
inverted switch
The first two conditionals are testing for the same condition in two different attributes. In my view, the most elegant way to detect if one thing matches one of a few other things is with a switch
. I’m accustomed to using a variable as the control expression and using hard-coded literals or constants as the cases; but we can invert that pattern:
switch "" {
case req.Foo, req.Bar:
return errInvalidRequest
}
The second two conditionals are dealing with attributes of two different types: one is an int64
and the other is a time.Time
. However, time.Time
can covert itself to an int64
with (time.Time).UnixNano()
. We can use that to apply the same pattern here:
switch int64(0) {
case req.ThingID, req.SomeDate.UnixNano():
return errInvalidRequest
}
The validate
func now looks like this:
func (req SomeRequest) validate() error {
switch "" {
case req.Foo, req.Bar:
return errInvalidRequest
}
switch int64(0) {
case req.ThingID, req.SomeDate.UnixNano():
return errInvalidRequest
}
return nil
}
Much better.
As a bonus, gocyclo
now calculates complexity at 3.
$ gocyclo .
3 main (SomeRequest).validate