Subtle GoLang concepts

Rahul Saha
TechVerito
Published in
5 min readMar 28, 2024

--

Photo by AbsolutVision on Unsplash

Preface

As any other developer I started my journey of Go with the tour. Though the tour serves as a great foundation there are many concepts in go which are not so obvious. In this article I have gone through many such concepts that I have encountered since I started my journey of go.

This article is for novice to intermediate experienced folks. It assumes you have some familiarity with Go.

Reference types

For some types, pointers need to be passed around to mutate the original object but for some types its not needed e.g slice, context. These seemingly magical types do this because they hold pointers to the underlying data structure. So pass by value is enough since pointers are copied but always point to the same location. One can very easily create their own reference types.

Eg. Slices are always 24bits in size. 1 addr pointer to underlying array, 1 size and 1 capacity value. Since it holds a pointer to the underlying array, slices can be passed around without need of pointers.

Pass values or pointers?

Go supports pointers like C but technically its only pass by value. When a pointer is passed in a function, a copy of the pointer value (the memory address) is passed.

Unless dealing with very large objects, prefer passing values instead of pointers. Passing pointers increases garbage collection overhead.

employee := Engineer{name="John"}

passByValue(employee)
passByPointer(&employee)

Value or pointer receiver?

The decision to choose between value and pointer receiver on a type should not depend on whether the func is going to mutate the value or not, it should be based on the nature of the type. And receivers should not be mixed.

If adding or removing something from a type should create another instance of the type, it should use value receiver. But in case it should mutate the instance, pointer receivers should be used.

E.g. Roughly speaking Point{x, y} type should use value receivers and pointer receiver is applicable on an Account{id, balance} type.

iTable

employee is interface pointing to engineer instance

Interfaces are two word data structures, consisting of an iTable address and an instance address. Structure of iTable is shown in the above diagram.

Don’t shy away from interfaces

Using a single method in an interface is a prevalent convention in Go. Typically, these interfaces are named with a suffix like *er, such as Reader and Writer. This aligns with the Interface Segregation Principle of the SOLID design principles.

type Writer interface {
Write(p []byte) (n int, err error)
}

Type Embedding

Go does not support inheritance but it does support composition via type embedding. Attributes of embedded type become available on the outer type as if they were declared on it.

type user struct{
Name string
Email string
}

type Admin struct{
user // embedded type
Rights int
}

a := Admin{
Rights: 3
}
a.Name = "John"
e.Email = "john@example.com"

Channels

different declarations of channels

Unbuffered channels and Synchronization

Channels can be either buffered or unbuffered. Buffered channels come with a buffer which allows the sender and receiver to independently access the channel and there is minimum coupling. Unbuffered channels need synchronization to work. Both sender and receiver must be available for the communication to work. If the sender goroutine is unavailable the receiver will wait till its available and vice versa.

While this may seem like an disadvantage, this behavior can be leveraged to synchronize goroutines.

Another advantage of unbuffered channels is it guarantees exchange happens. Send is complete only if it is received. A buffered channel has no such guarantee.

Cleanup of channels

When a channel is closed, goroutines can still read from the channel but can no longer write data to it. This allows a closed channel to be emptied.

Context switching in goroutines

As goroutine 1 makes a blocking call its removed from logical processor and assigned another thread

Goroutines are lightweight threads managed by the internal scheduler. They are especially great for IO bound tasks.

When a goroutine performs a blocking call, the thread and goroutine are detached from the logical processor. The scheduler attaches/creates a new thread to the logical processor. It will choose another goroutine from the local run queue. Once the blocking call is complete, the goroutine is placed back to the local run queue and the thread is put aside for future use.

If the blocking call happens to be a network call, it’s handled a bit differently. The goroutine is detached from the logical processor and moved to the integrated network poller. Once the poller notifies read/write is ready then goroutine is placed back to the logical processor to continue.

Error Handling

Golang does not provide ‘throwing’ or ‘raising’ errors like other languages and there is no stacktrace. This reduces runtime overhead but results in a boilerplate of ‘check if error’ blocks. This is a standard practice in go.

user, err := createUser("John Doe")

// Check if an error occurred
if err != nil {
// Handle the error
fmt.Println("Error creating user:", err)
return
}

err := user.sendMessage("hi")

// Check if an error occurred
if err != nil {
// Handle the error
fmt.Println("Error sending message:", err)
return
}

There is also a panic mechanism that immediately stops execution of the program. It should be used only in case of unexpected errors where the application can not gracefully recover from it. Panic incurs a performance overhead.

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()

// This function might panic
potentiallyPanicFunction()
}

func potentiallyPanicFunction() {
// Some code that might panic
panic("Something went wrong!")
}

Final words

Thanks for making it to the end of this article. Hope you had something to take away. I will keep updating this article with more topics as I come by. Peace out.

--

--

Rahul Saha
TechVerito

A software engineer from Kolkata, India. Also a linux enthusiast, photographer and accidental drawing artist.