- read

Golang tips and tricks

Kirill Luzhnov 1

Like it ;)

In the first part, we will delve deeper into Golang software engineering tech interview questions. But before that, I would like to tell you my story and how I got into the Golang world.

Hi, my name is Kirill, and I’m a software engineer with over 10 years of experience, working and living in Berlin. I first encountered Golang in 2015 and had no idea why anyone should use it or what purpose and principles were followed by the Go maintainers. After several years of using Go, I have now found answers to the most frequent questions that I would ask you as a software engineer being interviewed.

This collection is far from being a “full base of knowledge about Golang,” but there are enough answers to the questions that you will face during interviews.

Let’s GO!

Q: What is Go (Golang)?

A: Go is a statically typed, compiled high-level programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. It is syntactically similar to C, but with memory safety, garbage collection, structural typing, and CSP-style concurrency.

Q: What are the features and benefits of Go?

A:

  1. Simplicity — Go was designed to provide an easy-to-learn but efficient replacement for C and C++. The language is easy to learn and understand due to its simplicity and syntactic sugar.
  2. Concurrency — Go has built-in and optimized support for application concurrency, allowing programmers to create scalable code.
  3. Efficiency — Go is a compiled language that produces machine code, making it fast and efficient. Its built-in garbage collector speeds up development by eliminating the need for manual memory management.
  4. Open source — Go is an open source language, meaning that its source code is freely available and can be modified by anyone. This has helped to build a vibrant community around the language.
  5. Large standard library — Go comes with a large standard library that provides many useful functions for building network applications, web servers, and system software.

Q: What is a Slice in Go?

A: Slices are very similar to arrays in Go, but they are a more flexible design that allows you to change their size, unlike an array whose size must be determined during initialization.

Q: How do you declare a Slice in Go?

A: Simple example: myslice := []int{}

Q: What is a Map in Go?

A: Map is a built-in data structure that provides an unordered collection of key-value pairs. Maps are similar to dictionaries in other programming languages, and they are commonly used to represent associative arrays.

More about maps I would like to recommend you to check in the source: https://github.com/golang/go/blob/master/src/runtime/map.go#L117

Q: How do you declare a Map in Go?

A: var m map[string]int - empty map (and it’s better to create it via using make() function, in another way it will cause a runtime panic)

after the initiation you can use:

m := make(map[string]int) 
m["key"] = 1
m["example"] = 2

or use simpler ways:

var a = map[string]string{"key": "value"} 
b := map[string]int{"key": 1}

Q: What is the difference between a Map and a Slice in Go?

A: A slice is an ordered collection of elements of the same type, but a map is an unordered collection of key-value pairs, where each key must be unique.

  • Ordering: Slices guarantee the order of their elements, while the order of elements in maps is random.
  • Indexing: Slices are indexed by integers, while maps are indexed by keys of any hashable type.
  • Element types: Slices contain elements of a single type, while maps contain key-value pairs, where the value could be of a different type than the key.
  • Resizing: Slices can be resized using the built-in append function, while maps do not have a built-in resizing mechanism.
  • Accessing elements: Slices can be accessed using integer indices, while maps can be accessed using keys of any hashable type.

Q: What are Interfaces in Go?

A: An interface is a structure that describes methods that must be implemented by other structures in order to satisfy the interface. Matching to an interface is not explicit: it is sufficient to describe the implementation of the interface methods and the object itself. Without any further specification in the code, the object begins to satisfy the given interface.

Q: What is Polymorphism in Go?

A: In Go, polymorphism is achieved through the use of interfaces.

An interface in Go is a collection of method signatures that define a set of behaviors. Any type that implements all the methods of an interface is said to satisfy that interface. This means that any variable of that type can be used wherever the interface is expected.

Simple example of Shape interface:

type Shape interface { 
Area() float64
}

Any type that has an Area() method that returns a float64 value can satisfy this interface.

type Circle struct { 
Radius float64
}

func (c Circle) Area() float64 { // we just define return type and method name that should be exact like in interface
return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
Width float64
Height float64
}

func (r Rectangle) Area() float64 { // the same here - we did not define directly, that this method was inherrited, but we define name and type, and this is enought to follow **duck typing** principle
return r.Width * r.Height
}

Ok, now we have a definition of two different methods that implement Shape interface. Let’s define a new variable:

shapes := []Shape{Circle{Radius: 2.5}, Rectangle{Width: 2, Height: 3}} 

// and for example we can go through this slice and run the function

for _, shape := range shapes {
fmt.Println(shape.Area()) // this is the example of Polymorphism in go :)
}

Q: What is a Pointer in Go?

A: Another great topic was published here “Pointers in Golanghttps://medium.com/analytics-vidhya/pointers-in-golang-1a679b464849

A pointer is a variable that references a certain location in memory where a value is stored, so you take a variable, place it on memory then instead of reading the actual value, you just get the address of where that value is; When you pass it around the code, since it’s just a “number” it is cheaper to move than the actual value, which could be way longer than just the address (this will be important at the end of the article).

Q: What is the difference between a Pointer and a Reference in Go?

A: A pointer is a variable that stores the address of another variable, while a reference is not a type in Go. In Go, there is no reference type like in other programming languages such as C++. In Go, pointers are used to indirectly access and modify the value of a variable.

Q: How do you declare an Interface in Go?

A: There existing a good description with example https://medium.com/@adityapathak1189/interfaces-in-golang-59856589151c

Q: What is the defer statement in Go?

A: The keyword defer is used to delay the call of function.

A function with defer is always executed before the external function in which defer was declared exits. Everything that called in defer block will be called in reversed order, because defer put functions call into the lifo stack.

... 
for order:=0; order < 3; order++ {
defer fmt.Print(order) // will return 2 1 0, from last to first
}

! But! An important feature of calling functions via defer is that the values we pass to the deferred methods will be evaluated at the moment of execution of the main algorithm (in the example above - first the order will be increased, according to the condition 0->1->2), saved in memory, and used later, at the moment of calling the function itself.

Q: What is a Goroutine?

A: A goroutine is a lightweight block of code that works asynchronously. It is declared with the go operator before the function whose computation needs to be asynchronous. On a multicore architecture, goroutines can be executed on different cores of the processor, making these computations parallel and greatly speeding up the computation.

One of the main features of goroutines is their ability to communicate with each other through channels, which are built-in data structures in Go that allow goroutines to send and receive values from each other. This makes it easy to coordinate the execution of multiple goroutines and to share data between them.

Q: How does Goroutine differ from Thread?

A: Goroutine is independent of the system, unlike thread. Gorutines are considerably lightweight and don’t require more resources.

In comparison, we can run significantly more goroutines than threads at the same time — Goroutines are multiplexed onto OS threads, rather than a 1:1 mapping.

Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management. https://tip.golang.org/doc/effective_go#goroutines

Goroutines startup more quickly than operating system threads.

Q: What is a Channel in Go?

A: A channel is a built-in structure for data exchange and communication between goroutines. This is one of the core features of the language I would recommend that you become as familiar as possible with how channels work, why and when they are used. This is a very important part of concurrency in Golang

Better overview you can get from the source from runtime/chan.go or from official repo https://github.com/golang/go/blob/master/src/runtime/chan.go

Q: How do you declare a new Channel in Go?

A: Simple example: ch := make(chan string)

Q: What are the different types of Channels in Go?

A: In Go there are two main types of channels: unbuffered and buffered.

Unbuffered channels:

An unbuffered channel is a channel that can only hold one value at a time. When a value is sent on an unbuffered channel, the sending goroutine will block until the value is received by another goroutine. Similarly, when a value is received from an unbuffered channel, the receiving goroutine will block until a value is sent on the channel.

Buffered channels:

ci := make(chan int) // create unbuffered channel of integers

// start two goroutines
go func() {
ch <- 42 // send a value on the channel
}()
go func() {
val := <- ch // receive a value from the channel
fmt.Println(val)
}()

A buffered channel is a channel that can hold a fixed amount of values. When a value is sent on a buffered channel, the sending goroutine will not block as long as there is space in the channel’s buffer. When the buffer is full, any further sends will block until values are received from the channel. Similarly, when a value is received from a buffered channel, the receiving goroutine will not block as long as there are values in the channel’s buffer. When the buffer is empty, any further receives will block until values are sent on the channel.

ch := make(chan int, 3) // create a buffered channel of integers, buffer size is 3

// start three goroutines that send values on the channel
go func() {
ch <- 1
}()
go func() {
ch <- 2
}()
go func() {
ch <- 3
}()

// start a goroutine that receives values from the channel
go func() {
for val := range ch {
fmt.Println(val)
}
}()

Q: What operations can be performed on the channels?

A: Four types of operations can be performed with the channel in the GO:

  1. Creating a channel ch := make(chan int)
  2. Writing something to a channel ch <- 1
  3. Reading from a channel <- ch
  4. Closing the channel close(ch)

Other operations directly related to channel operation, such as reading or writing to a nil channel, or to a closed channel, or to a buffered channel, can be found in the official repository as well: https://github.com/golang/go/blob/master/src/runtime/chan.go#L160

Q: What is the difference between a channel and a semaphore in Golang?

A: In Go, channels and semaphores are both mechanisms for managing concurrent access to shared resources, but they work in slightly different ways.

A channel is a way to communicate between goroutines, allowing one goroutine to send a value to another goroutine. Channels are implemented as a first-class construct in Go, and provide synchronization and communication between goroutines. When a goroutine sends a value on a channel, it blocks until another goroutine receives the value from the channel.

A semaphore, on the other hand, is a low-level synchronization primitive that allows multiple goroutines to access a shared resource concurrently. Semaphores work by maintaining a count of the number of available resources, and blocking or allowing access based on the count. In Go, semaphores can be implemented using channels or the sync package.

So while both channels and semaphores can be used for synchronization and managing concurrent access to shared resources, channels are primarily used for communication between goroutines, while semaphores are primarily used for controlling access to shared resources. Additionally, channels provide a higher-level abstraction and more safety guarantees, while semaphores are more low-level and require more careful management.

package main

import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup
semaphore := make(chan struct{}, 5) // create a semaphore with capacity 5

for i := 0; i < 10; i++ {
wg.Add(1)
semaphore <- struct{}{} // acquire a semaphore slot
go func(id int) {
defer func() {
<-semaphore // release the semaphore slot
wg.Done()
}()
// do some work
fmt.Printf("Worker %d starting\n", id)
// simulate some work with a sleep
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}(i)
}

wg.Wait()
fmt.Println("All workers done")
}

In this example, we create a WaitGroup and a channel called semaphore with a capacity of 5, which acts as our semaphore. We then start 10 goroutines, each of which acquires a slot in the semaphore by writing a value to the channel, and releases it by reading from the channel after the work is done.

The WaitGroup is used to wait for all goroutines to finish before exiting the program. The Add() method is called to increment the count of the WaitGroup, and the Done() method is called to decrement the count when the goroutine finishes.

Note that the use of a struct{} type as the value of the semaphore channel is a common idiom in Go for representing an empty struct, since it takes up no memory and has no fields. The use of time.Sleep() is just a simple way to simulate some work being done by the goroutine.

Q: How does Go manage memory?

Perfect description about “Memory management in Go” you can find here https://medium.com/@ali.can/memory-optimization-in-go-23a56544ccc0

Go allocates memory in two places: a global heap for dynamic allocations and a local stack for each goroutine. One major difference Go has compared to many garbage collected languages is that many objects are allocated directly on the program stack. Go prefers allocation on the stack

Q: What are some popular Go web frameworks?

A:

  1. Gin — A lightweight and fast framework that uses httprouter to provide a fast HTTP router and middlewares for things like logging, recovery, and validation. https://github.com/gin-gonic/gin
  2. Echo — A fast and minimalist framework that emphasizes performance and ease of use. It also includes features like routing, middleware, and validation. https://github.com/labstack/echo
  3. Beego — A full-featured MVC framework that provides ORM, caching, sessions, and other features out of the box. It also includes support for auto-generated API documentation. https://github.com/beego/beego
  4. Revel — A full-stack web framework that follows the Model-View-Controller (MVC) pattern and includes features like routing, templating, testing, and validation. https://revel.github.io/
  5. Iris — A fast and flexible framework that emphasizes high performance and developer productivity. It includes features like routing, middleware, and templating. https://github.com/kataras/iris
  6. Buffalo — A framework that aims to make it easy to build modern web applications in Go. It includes features like routing, asset management, database migrations, and more. https://github.com/gobuffalo/buffalo

Personally, I would like to add that I prefer not to use go frameworks and avoid orm and other things. But it depends on the specific problem that this or that your service solves.

Q: What is garbage collection in Golang, and how does it work?

A: Once memory has been allocated and is no longer in use, it requires cleanup. In GO, a garbage collector is employed to handle this task, using the Mark and Sweep algorithm. This algorithm consists of two main components — Mark and Sweep. In the Mark phase, active variables are scanned according to a three-color marking system (white, gray, and black). Initially, all memory locations are marked as white, and then both used and unused objects are marked as the memory is traversed. In the Sweep phase, the entire memory is scanned from beginning to end, and all blocks, whether free or in use, are examined. The used blocks are marked black, while the unused ones are marked white. Finally, in the Sweep stage, the white-marked blocks are cleaned up. Therefore, whenever the heap is modified, a garbage collector is triggered, which scans through the memory and eliminates any unused areas.

Let’s take a moment to reflect here. As you can see, this is not a comprehensive list, and there is much more to explore. However, I will follow up with a second, and possibly even a third, part in the near future. In the meantime, I encourage you to subscribe and like this post. Please feel free to leave a comment as I value your feedback and would be happy to update this document with any new insights or corrections you may have. Additionally, if you have any questions that you are struggling to answer, please leave them in the comments, and I will do my best to assist you.