Avoiding Runtime Exceptions in Golang

Midhun
Custom App Development
5 min readFeb 2, 2024

Even though Go is a statically typed language, it’s essential to avoid runtime panics or crashes in Golang. To help prevent these issues, we should follow certain best practices.

First, always return an error from a function that can fail. Typically, the error is returned as the last return value of the function. When consuming that function, be sure to check for error values before proceeding.

result, err := aFunction(input)
if err != nil {
//handle the error
//either you can terminate the program using log.Fatal or return the
//error from the function for the parent function to decide.
return err // return the error for the parent function to handle.
}

error type is a built-in interface in Golang. Any type that implements the interface below can be used as an error value:

type Error interface {
Error() string
}

It’s easy to write your custom error type by implementing the above interface. The standard library errors package has some nice functions to handle errors easily. A called function can wrap an error with another error to add more contextual information to pass it on to the parent function. This can be done using the fmt.Errorf function and using the %w verb as shown below.

var newError = fmt.Errorf("New error from some function %w",oldError).

To create a new error, you use the errors.New(“some error”) function.

You can check if an error wraps another error or not using this logic:

errors.Is(someWrappedError, someErrorType )

Another best practice is to use the defer statement, which allows you to postpone the execution of a function until the surrounding function (containing the defer statement) is completed. This is similar to the destructor function in object-oriented languages like C++.

func someFuntion() {
defer func(){
//clean up the resources taken in the surrounding function to clear memory
//this is called at the end of the 'someFucntion' when it returns
}()
//
//do stuff
}

You can also use the recover function inside a defer function to recover from any panic from the surrounding function.

func someFuntion() {
defer func(){
if r := recover(); r!=nil{
//recoverd from panic handle it gracefully
}
}()

///do stuff
}

Always check for nil before using a pointer type. When you try to dereference a ‘nil’ pointer, it will create runtime panic and crashes. Checking ‘nil’ pointers helps to ensure that the pointer is pointing to a valid memory.

Here is an example illustrating this concept:

//pointer to an integer
var somePointer *int

var a int = 10. //integer variable

somePointer = &a // initializing the pointer to point to 'a'. Valid memory

if somePointer != nil {
//succeeds as somePointer points to 'a'
fmt.Printf("pointer value %d", *somePointer)
}

In the above example, we define a pointer to an ‘int’ on this line “var somePointer *int” and we initialize the pointer to point to a valid memory on this line “somePointer = &a”. Here, we are saving the address of the variable ‘a’ with the pointer variable ‘somePointer’. It’s important to note that uninitialized pointers in Go are automatically initialized to ‘nil’. So, it’s important to initialize a pointer before using it and it’s a recommended practice to check if a pointer variable is ‘nil’ before dereferencing it.

You can initialize a pointer by assigning an address of another variable of the same type to it as shown below.

var name string = "David"
var p *String = &name

Here ‘p’ is a pointer to a string pointing to ‘name’. In Golang, you can also initialize a pointer using the ‘new’ keyword. The ‘new’ keyword is used to create a new instance of a variable and initialize it to its zero value. The ‘new’ keyword returns a pointer to that type by creating an instance of that type and initializing it to its zero value.

For numeric types (int, float, etc) the zero value is 0.

For string types, the zero value is the empty string (“”).

For Boolean types the zero value is false.

For composite types (struct, array, maps, slices) the ‘new’ will initialize each element to its zero value.

For ex:

type Person struct {
name string
age int
}
p := new(Person)
fmt.Println(p) // prints { 0} as the zero value of name is ""

Always use the ‘make’ syntax to initialize memory for a map or slice before using it. ‘new’ is used to create pointers to a slice and a map, but if you use these pointers without proper initialization, it will result in runtime panics. The preferred way is to use ‘make’ function to initialize slices and maps before using them.

‘make’ initializes and returns a ready-to-use slice or map. ‘new’ will only allocate the memory and return a pointer but does not properly initialize the slice or map so always use ‘make’ to properly create and initialize a map or slice.

For example:

var age map[string]int
// allocates memory for the map. Do not use the map before allocating memory for it.
age = make(map[string]int)

age["David"] = 34
fmt.Printf("Age map %v", age)

Always check for the ‘nil’ value for a map key. This means checking if the key exists in a map data structure before using it, as shown below:

If value,ok := age["David"];ok{
//got the value use it
fmt.Printf("")
}else{
//key does not exist. Insert it or return
}

In the above example, if we do not check if the key(“David”) exists in the map and directly use it, then it will result in runtime panic and a crash if the map does not contain this key.

When working with channels, remember to close the channel by a goroutine that’s writing into it. A goroutine that’s reading from a channel should never close it. The general rule is that the goroutine that writes data to a channel is the one responsible for closing it and releasing the memory. A goroutine that reads data from a channel should never close it. You use the ‘close()’ function to close a channel.

The receiver goroutine uses the two-value receive operation value,ok:= <- dChannel to check if the channel has been closed. If ok is false, it means the channel is closed, and the receiver can exit the loop.

data,ok := <- dChannel

if !ok {
//channel closed
}

Lastly, use static analysis tools like GoKart to find vulnerabilities upfront. It’s also a good idea to test your Go program by running it with the -race flag to detect possible race conditions upfront before pushing it to production.

‘go run -race yourprogram.go’

GoKart on GitHub: https://github.com/praetorian-inc/gokart

Develop your app with Zco: https://www.zco.com/mobile-app-development/

Learn why developers use Go for web servers: https://www.zco.com/blog/why-developers-use-go-language-for-web-servers/

--

--

Midhun
Custom App Development

Midhun is a Senior Technical Manager at Zco. Loves programming in Rust and Golang. In free time works on Rust and GoLang projects for fun.