How To Write Maintainable Go Code?
- With Code Example
- August 21, 2024
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.
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.
- Medium — https://medium.com/@harendra21
- Twitter (X) — https://x.com/harendraverma2