These are a few tips on how to make Go code more readable and robust. Go already has great resources on how to write code, such as Effective Go, Go Code Review Comments and Practical Go, but these are my preferences and observations from writing and reviewing Go code over the last few years. Many of these points are subtle, but I think they enhance the overall readability and maintainability of a code base.

1. False

When checking for false, prefer this:

if myVariableName == false {
   sendEmail("oh no")
}

to this:

if !myVariableName {
   sendEmail("oh no")
}

The reason is that when scanning through code it's easy to miss the !; typing a few extra characters makes it more readable.

2. Switches

When choosing among several possible outcomes, prefer a switch statement to an if statement, like this:

switch {
case f.IsList():
  return "it's a list"
case f.IsMap():
  return "it's a map"
default:
  return "no idea"
}

... not this:

if f.IsList() {
  return "it's a list"
else if f.IsMap() {
  return "it's a map"
} else {
  return "no idea"
}

The switch statement lays out the options more clearly, and your eye doesn't have to follow the logic of "if/else if/else". The switch statment places everything in a vertical line.

3. Early exit

Get negative tests out of the way early in a function, and exit, rather than creating nested if statements. For example, instead of this:

if len(grades) > 0{
  var sum float64  
  for _, g := range grades {
    sum += g
  }
  return sum / len(grades)
}

...do this:

if len(grades) == 0{
  return 0
}

var sum float64  
for _, g := range grades {
  sum += g
}
return sum / len(grades)

This keeps the eye going down in a vertical line, and avoids having to think about the meaning of the nested levels.

4. Call a function instead of commenting

While writing a function, it's common to end up with a few blocks of logic in the middle. They do some kind of processing on the data we received before returning a result. Sometimes an intermediate result is involved, then we do something else to it, etc. If you find yourself starting to write a comment to explain what the block does, refactor it to its own function. The function name should be all the documentation you need.

For example, consider this function that determines if a student is eligible to graduate:

func eligibleForGraduation(s *Student) error {

  var unclearedHolds int 
  for _, hold := range s.Holds {
    if hold.Status != cleared {
      unclearedHolds++
    }
  }

  if unclearedHolds > 0 {
    return fmt.Errorf("student has %d uncleared holds", unclearedHolds)
  }

  if len(s.Grades) == 0{
	return fmt.Errorf("student GPA is less than 2.0")
  }

  var sum float64  
  for _, g := range s.Grades {
	sum += g
  }
  gpa := sum/len(s.Grades)

  if gpa >= 2.0 {
    return nil
  } else {
    return fmt.Errorf("student GPA is less than 2.0")
  }

  return false 
}

This function is quite short, but imagine if there were five or six more graduation requirements; it would quickly become hard to read. Furthermore, the logic of determining the intermediate values (such as GPA) is inline with the logic that uses the values to implement the process logic.

You might be tempted to do this to help the next reader:

func eligibleForGraduation(s *Student) error {

  // check if student has any holds on his account
  var unclearedHolds int 
  for _, hold := range s.Holds {
    if hold.Status != cleared {
      unclearedHolds++
    }
  }

  if unclearedHolds > 0 {
    return fmt.Errorf("student has %d uncleared holds", unclearedHolds)
  }


  // ensure GPA meets the minimum for graduation
  if len(s.Grades) == 0{
	return fmt.Errorf("student GPA is less than 2.0")
  }

  var sum float64  
  for _, g := range s.Grades {
	sum += g
  }
  gpa := sum/len(s.Grades)

  if gpa >= 2.0 {
    return nil
  } else {
    return fmt.Errorf("student GPA is less than 2.0")
  }

  return false 
}

This helps, by separating chunks of code and explaining what they do so the reader doesn't have to review each loop, but it doesn't solve the problem of mixing process logic (is the student eligible to graduate?) and the logic to get the intermediate values.

If we rewrite it like this instead, it is easier to understand exactly what the graduation requirements are:

func eligibleForGraduation(s *Student) error {

  if unclearedHolds(s) > 0 {
    return fmt.Errorf("student has %d uncleared holds", unclearedHolds)
  }

  if gpa(s) < 2.0 {
    return fmt.Errorf("student GPA is less than 2.0")   			
  }

  return nil
}

We can also now document the code func separately where it is defined, test it separately, and re-use it elsewhere, etc.

5. Zoom out

Often when we review a pull request, we're presented with a few changes in a few files, or maybe a function or a type is introduced and used. Without comments it can be very hard to understand why the code is written the way it is. Sure, if you read the code you can figure out what the new function does, but it doesn't tell you why we're doing it in the first place.

Begin by commenting the new function and say what it does and why you had to create it. Then, put it in larger context (zoom out). If we have a function called GetLastCustomerStatusChange, in the pull request explain why we added that function ("Added GetLastCustomerStatusChange to determine if customer lifetime value needs to be updated"). This allows the pull request reviewer to weigh in: is that the best way to determine if the value must be recalculated? What other cases can occur? Do we have another, existing way to determine this?