How To Write Maintainable Go Code?

Discover the Easy Secrets of Writing Maintainable Go Code!

Hi everyone, Gophers You have to write code that can be maintained when dealing with golang. Simplicity, readability, and clarity are the fundamental components of maintainable code; these qualities help new team members grasp your work. When an individual departs, it facilitates their ability to swiftly and effortlessly continue where they left off in terms of code. If you want to write more maintainable code, you might want to think about applying these strategies.

1up3ae7l3rbc2uhtpx29pxg_ZuoZijJ1_3698069266298698132.webp

Originally Published at - medium.com

Table of Contents

Keep the main as small as possible

As we all know the main is the first goroutine function at the start of your go program, so we call it main goroutine. It is unique because the main package is compiled into an executable program rather than being treated like a regular package by the compiler and has exported names. The main function (main.main), which serves as a Go program’s entry point, is located inside the main package. The primary function and package are expected to perform as little as feasible.

It is strongly advised not to implement the business logic inside the main package and instead to drive the program with main.main since it is difficult to write tests for the code inside main.main. The structure and maintainability of the software are enhanced by separating the driver and business logic into different packages.

package main  
  
import (  
 "fmt"  
 "log"  
)  
  
// main should only handle high-level orchestration  
func main() {  
 // Initialize the application  
 app, err := initializeApp()  
 if err != nil {  
  log.Fatalf("Failed to initialize the app: %v", err)  
 }  
  
 // Run the application  
 if err := app.run(); err != nil {  
  log.Fatalf("Application error: %v", err)  
 }  
}  
  
// Application struct encapsulates the application's state  
type Application struct {  
 config string // example field  
}  
  
// initializeApp initializes the application and returns an instance of it  
func initializeApp() (*Application, error) {  
 // Simulate loading configuration  
 config, err := loadConfig()  
 if err != nil {  
  return nil, err  
 }  
  
 // Create and return an Application instance  
 return &Application{config: config}, nil  
}  
  
// run starts the application logic  
func (app *Application) run() error {  
 // Simulate running the application  
 fmt.Println("Running application with config:", app.config)  
 return nil  
}  
  
// loadConfig simulates loading the configuration for the application  
func loadConfig() (string, error) {  
 // Simulate configuration loading  
 config := "App Configuration"  
 return config, nil  
}

Always use meaningful names

In Golang, it is crucial to provide variables, functions, structs and interfaces meaningful names. Thus, we have selected significant names and clarify what they stand for. Do not just pick names at random that have no connection to the topic. Though picking a name might occasionally be very difficult, you will not regret it because it makes your code easier to understand and read for both you and other people.

Example with bad names

package main  
  
import (  
 "fmt"  
 "math"  
)  
  
// m function performs some calculations and prints results  
func m() {  
 r := 5.0  
 a := cA(r)  
 fmt.Printf("The result is %.2f\n", a)  
  
 s := 4.0  
 b := sA(s)  
 fmt.Printf("The result is %.2f\n", b)  
}  
  
// cA calculates something related to circles  
func cA(r float64) float64 {  
 return math.Pi * r * r  
}  
  
// sA calculates something related to squares  
func sA(s float64) float64 {  
 return s * s  
}

You will see that in this example, the names do not indicate what we hope to accomplish in this section of code, which is highly problematic for both reliable and well-written code. Nobody will understand what we want to do with this code when they see this, and eventually, it will be too late for you to grasp either.

Example with meaningful names

package main  
  
import (  
 "fmt"  
 "math"  
)  
  
// main function orchestrates the calculation and output  
func main() {  
 radius := 5.0  
 circleArea := calculateCircleArea(radius)  
 fmt.Printf("The area of the circle with radius %.2f is %.2f\n", radius, circleArea)  
  
 sideLength := 4.0  
 squareArea := calculateSquareArea(sideLength)  
 fmt.Printf("The area of the square with side length %.2f is %.2f\n", sideLength, squareArea)  
}  
  
// calculateCircleArea computes the area of a circle given its radius  
func calculateCircleArea(radius float64) float64 {  
 return math.Pi * radius * radius  
}  
  
// calculateSquareArea computes the area of a square given the length of one side  
func calculateSquareArea(sideLength float64) float64 {  
 return sideLength * sideLength  
}

Try to write meaningful comments wherever possible

The only method to give a sense of what you want to do with the code is to provide comments, which are a more accurate approach to understanding what is occurring with the code. As a result, you must include thorough comments with every significant part of the code.

Not only is it crucial to write comments, but we also need to update the comments anytime we update the code to avoid being misled. Block comments and inline comments are both supported in Golang. Additionally, as it is illogical to explain self-explanatory code, I would caution against making excessive comments on the code.

package main  
  
import (  
 "errors"  
 "fmt"  
)  
  
// main is the entry point of the application  
func main() {  
 numbers := []int{1, 2, 3, 4, 5}  
  
 // Calculate the sum of the slice elements  
 sum := calculateSum(numbers)  
 fmt.Printf("Sum of numbers: %d\n", sum)  
  
 // Find the maximum value in the slice  
 max, err := findMax(numbers)  
 if err != nil {  
  fmt.Printf("Error: %v\n", err)  
 } else {  
  fmt.Printf("Maximum value: %d\n", max)  
 }  
}  
  
// calculateSum adds up all the integers in the provided slice  
// It returns the total sum of the elements  
func calculateSum(nums []int) int {  
 sum := 0  
  
 // Iterate over each number in the slice and add it to the sum  
 for _, num := range nums {  
  sum += num  
 }  
  
 return sum  
}  
  
// findMax returns the maximum value in the provided slice of integers  
// If the slice is empty, it returns an error  
func findMax(nums []int) (int, error) {  
 // Check if the slice is empty  
 if len(nums) == 0 {  
  return 0, errors.New("slice is empty, cannot determine maximum value")  
 }  
  
 // Assume the first element is the maximum  
 max := nums[0]  
  
 // Iterate over the slice to find the actual maximum  
 for _, num := range nums[1:] {  
  if num > max {  
   max = num  
  }  
 }  
  
 return max, nil  
}

Don’t repeat the piece of code

Writing the same code over and over again is a very terrible habit in all programming languages, not just Golang. The DRY (Do not Repeat Yourself) concept explicitly forbids using this technique. I am going to make a separate function if I need the same code in at least two different places.

Repeatedly writing the same code may cause overhead when it comes time to update it, forcing us to change it everywhere and sometimes missing it altogether.

Example of bad practice

package main  
  
import (  
 "fmt"  
)  
  
// main function demonstrates the bad practice of code repetition  
func main() {  
 // Repeating the calculation for different sets of numbers  
 num1, num2 := 3, 5  
 sum1 := num1 + num2  
 product1 := num1 * num2  
 fmt.Printf("Sum of %d and %d is: %d\n", num1, num2, sum1)  
 fmt.Printf("Product of %d and %d is: %d\n", num1, num2, product1)  
  
 num3, num4 := 10, 20  
 sum2 := num3 + num4  
 product2 := num3 * num4  
 fmt.Printf("Sum of %d and %d is: %d\n", num3, num4, sum2)  
 fmt.Printf("Product of %d and %d is: %d\n", num3, num4, product2)  
  
 num5, num6 := 7, 9  
 sum3 := num5 + num6  
 product3 := num5 * num6  
 fmt.Printf("Sum of %d and %d is: %d\n", num5, num6, sum3)  
 fmt.Printf("Product of %d and %d is: %d\n", num5, num6, product3)  
}

Example of good practice

package main  
  
import (  
 "fmt"  
)  
  
// main function demonstrates the good practice of avoiding code repetition  
func main() {  
 // Using a reusable function to calculate and display results  
 displayResults(3, 5)  
 displayResults(10, 20)  
 displayResults(7, 9)  
}  
  
// displayResults calculates the sum and product of two numbers and prints the results  
func displayResults(a, b int) {  
 sum := a + b  
 product := a * b  
 fmt.Printf("Sum of %d and %d is: %d\n", a, b, sum)  
 fmt.Printf("Product of %d and %d is: %d\n", a, b, product)  
}

Don’t use deep nesting in your code

Everyone may find it annoying when there are too many nested codes used because it makes the code harder to read. Therefore, it is quite challenging to understand code — whether it was developed by us or by someone else — when reading hierarchical code. Always strive to keep the code from repeatedly nesting.

Example of bad practice

package main  
  
import "fmt"  
  
// main function demonstrates the bad practice of deep nesting  
func main() {  
 number := 15  
  
 if number > 0 {  
  if number%2 == 0 {  
   if number > 10 {  
    fmt.Println("The number is positive, even, and greater than 10.")  
   } else {  
    fmt.Println("The number is positive, even, and 10 or less.")  
   }  
  } else {  
   if number > 10 {  
    fmt.Println("The number is positive, odd, and greater than 10.")  
   } else {  
    fmt.Println("The number is positive, odd, and 10 or less.")  
   }  
  }  
 } else {  
  fmt.Println("The number is not positive.")  
 }  
}

Example of good practice

package main  
  
import "fmt"  
  
// main function demonstrates the good practice of avoiding deep nesting  
func main() {  
 number := 15  
  
 if number <= 0 {  
  fmt.Println("The number is not positive.")  
  return  
 }  
  
 describeNumber(number)  
}  
  
// describeNumber prints the description of the number based on its value  
func describeNumber(number int) {  
 if number%2 == 0 {  
  describeEvenNumber(number)  
 } else {  
  describeOddNumber(number)  
 }  
}  
  
// describeEvenNumber handles even numbers  
func describeEvenNumber(number int) {  
 if number > 10 {  
  fmt.Println("The number is positive, even, and greater than 10.")  
 } else {  
  fmt.Println("The number is positive, even, and 10 or less.")  
 }  
}  
  
// describeOddNumber handles odd numbers  
func describeOddNumber(number int) {  
 if number > 10 {  
  fmt.Println("The number is positive, odd, and greater than 10.")  
 } else {  
  fmt.Println("The number is positive, odd, and 10 or less.")  
 }  
}

Return early and use conditions very wisely

In programming Return early is the practice where we need to return from the function as soon as we achieve the goal to avoid the unnecessary processing with the code and reduce the code complexity. It also helps to prevent deep nesting which I already explained in the above example.

Example of bad practice

package main  
  
import "fmt"  
  
// checkTemperature categorizes the temperature into different ranges  
func checkTemperature(temp int) string {  
 if temp < 0 {  
  return "Freezing cold"  
 } else {  
  if temp >= 0 {  
   if temp < 15 {  
    return "Cold"  
   } else {  
    if temp >= 15 && temp < 25 {  
     return "Mild"  
    } else {  
     if temp >= 25 {  
      if temp < 35 {  
       return "Warm"  
      } else {  
       return "Hot"  
      }  
     } else {  
      return "Error"  
     }  
    }  
   }  
  } else {  
   return "Error"  
  }  
 }  
}  
  
func main() {  
 temp := 22  
 fmt.Println("The weather is:", checkTemperature(temp))  
}

Example of good practice

package main  
  
import "fmt"  
  
// checkTemperature categorizes the temperature into different ranges  
func checkTemperature(temp int) string {  
 if temp < 0 {  
  return "Freezing cold"  
 }  
  
 if temp >= 0 && temp < 15 {  
  return "Cold"  
 }  
  
 if temp >= 15 && temp < 25 {  
  return "Mild"  
 }  
  
 if temp >= 25 && temp < 35 {  
  return "Warm"  
 }  
  
 return "Hot"  
}  
  
func main() {  
 temp := 22  
 fmt.Println("The weather is:", checkTemperature(temp))  
}

Use switch case more often instead of if/else

The switch statement is the most effective way to reduce the length of functions in comparison with the if-else statement. The switch case statement directly executes the matching condition and exits the code to avoid unnecessary processing but the if-else statement checks all conditions until it matches with the one which causes unnecessary processing.

Switch statement also helps to improve the code readability and reduce the code complexity. Switch cases provide ease of maintenance, performance optimization, and easy debugging.

Example of bad practice

package main  
  
import "fmt"  
  
// categorizeTemperature categorizes the temperature using if-else  
func categorizeTemperature(temp int) string {  
 if temp < 0 {  
  return "Freezing cold"  
 } else if temp >= 0 && temp < 15 {  
  return "Cold"  
 } else if temp >= 15 && temp < 25 {  
  return "Mild"  
 } else if temp >= 25 && temp < 35 {  
  return "Warm"  
 } else {  
  return "Hot"  
 }  
}  
  
func main() {  
 temp := 22  
 fmt.Println("The weather is:", categorizeTemperature(temp))  
}

Example of good practice

package main  
  
import "fmt"  
  
// categorizeTemperature categorizes the temperature using switch  
func categorizeTemperature(temp int) string {  
 switch {  
 case temp < 0:  
  return "Freezing cold"  
 case temp >= 0 && temp < 15:  
  return "Cold"  
 case temp >= 15 && temp < 25:  
  return "Mild"  
 case temp >= 25 && temp < 35:  
  return "Warm"  
 default:  
  return "Hot"  
 }  
}  
  
func main() {  
 temp := 22  
 fmt.Println("The weather is:", categorizeTemperature(temp))  
}

Continuous code refactoring

It is crucial to periodically restructure the code in huge codebases and update it to reflect new developments. Golang is constantly changing it, thus it is necessary to update the codebase to the most recent version and apply the most recent modifications when needed. One of the most crucial aspects of code maintenance is the ability to maintain our code more modern and secure through refactoring.

Conclusion

By following the small steps we can improve the code quality and maintainability of the code. There are small steps but very effective and I think most of them are not only stick to the golang, you can implement the same concepts in Java, Python, JavaScript, or any programming language, and it will be almost the same for others.

Thank you for reading this blog post, if you are interested in learning more about golang or full-stack development, consider following me. I keep writing posts about it regularly.