So you've:
You should be ready to land that job right... ❓
Maybe!
But here’s the thing. Some interviewers may ask you some more specific questions about Go (Golang) before they get into the actual coding and technical problem-solving.
Think of it like a gatekeeper. It's easy to put "Golang" as a skill on your resume but they want to make sure you actually know what you're talking about before they move on to more technical stuff.
Don't worry though. As long as you've been using Golang and you read through this post, then you should be fine. Often these questions are just basic concepts related to Golang, but the last thing you want, is to be surprised by these during your interview!
So what do you need to know?
Well, rather than make you sit down and try to learn 150+ Golang questions, I've put together the 58 most essential questions and answers that cover everything from fundamental concepts to advanced programming techniques with Go.
Understand these and you'll be able to answer almost any Golang based question they throw at you.
Huzzah!
Sidenote: If you find that you’re struggling with the questions in this guide, or perhaps feel that that you could use some more training and want to build some more impressive projects for your portfolio, then come and check out my complete Golang Developer course.
It’ll not only set you on the fast track to getting hired, but you’ll also blow away your interviewers with the level of work and knowledge you have.
With that out of the way, let’s get into the questions.
These questions test your basic knowledge of the Go programming language.
They are intentionally simple and the interviewer will use these questions to gauge if you have programmed with Go before.
If you are having trouble with these questions, then you should write some more Go code until you are confident in your ability to get through this section.
Go (otherwise known as Golang) is a programming language developed by Google, with built-in language support for creating concurrent programs using goroutines.
It was originally designed to be familiar to programmers who have learned the C programming language. Thanks to this, the simplicity of the language makes for a faster learning experience.
Go has multiple advantages:
A Go package is a collection of source files contained within a single directory, and all Go code exists within a package. Whereas a Go module is a collection of packages.
An example of a package named P
would be a directory P
containing a.go
and b.go
.
An example of a module would be a directory containing at least one package, and a go.mod
file must exist in the root module directory to delineate the directory as a module. The go.mod
file contains extra module information such as the dependencies in use and the name of the module.
Go has a garbage collector which periodically deletes allocated objects when they are no longer needed. This happens automatically without any input from the developer.
Go compiles to native machine code and does not require interpretation while running.
Since native machine code gets executed by the CPU directly without any translation layer, it executes faster than interpreted code.
GOPATH
and GOROOT
?They are both environment variables which serve different purposes:
GOROOT
is the path to the location of the Go tools.
GOPATH
is the path to the Go workspace where Go code gets stored.
Go modules are superseding GOPATH
usage and modern Go projects should use modules instead of relying on GOPATH
. Although GOPATH
is still used for storing code downloaded as dependencies.
A structure is a way to group data into a single unit. Structures have fields, and each field has a data type. Here is some sample code to define a new structure:
type Person struct {
age int
name string
}
A structure can get instantiated like this:
person := Person{}
This will create a new instance of Person
having default values. To set the initial values during creation, include them within the curly braces:
person := Person{
age: 22,
name: "Mike",
}
for
and what are the different forms?A for
loop is the standard way to perform repetitive operations in Go.
It has three components:
Combining these three components allows for fine-grained control over how the loop executes.
The first form uses all three components similar to a C-style loop:
for i := 0; i < 10; i++ {
//...
}
This loop starts at 0
, loops as long as i
is less than 10
, and increments the value of i
at the end of each iteration.
The second form of a for
loop forgoes the initialization statement and post statement, instead making exclusive use of the condition component. The developer needs to manage loop control manually to ensure that the condition will cause loop termination at some point during iteration:
for i < 10 {
// ...
}
The third form of a for
loop is an infinite loop and doesn't use any
components. It solely uses the for
keyword and the loop body:
for {
// ...
}
Since the loop is infinite, terminating the loop requires using the break
or
return
keywords (or a program crash).
switch
in Go, and what are the different forms?A switch is like a list of if..else if
blocks that instead uses case
statements where each case is a possible value of a variable.
This makes it easy to check a variable against different values. Switches in Go can also evaluate a condition instead of checking for variable equality.
The basic form of a switch checks to see if a variable is equal to some value:
switch n {
case 1:
// ...
case 2:
// ...
case 3, 4, 5:
// ...
default:
// ...
}
The "conditional" form of a switch will evaluate an expression for each case:
switch n := someFunc() {
case n < 10:
// ...
case n == 10:
// ...
case n > 10:
// ...
}
range
keyword in a for
loop?Using range
in a for
loop allows iterating through a collection without the
need to check the length of the collection.
range
can also provide copies of elements in the collection which allows the developer to access data without needing to use an index.
Prefer using range
because it makes improperly accessing a collection with an out-of-bounds index more difficult.
Errors are value types in Go and they get returned from functions using multiple return values.
The caller of the function should check the error returned from a function to determine if an error occurred. If an error occurred, then the developer should write code to handle this situation.
Go has the "comma,ok" idiom which gets used for error checking:
val, err := someFunc()
if err != nil {
// error occurred
}
Go supports multiple return values from functions using this syntax:
func multi() (int, int) {
return 1, 2
}
a, b := multi()
Since Go doesn't have exceptions, returning multiple values from a function allows Go to return an error if one occurs. This makes working with fallible functions easier.
For example
Instead of returning nil
from a function and having it mean "some ambiguous error occurred", we can instead return nil, error
and gain access to extra error information.
Using commas between variable names allow the declaration of more than one variable in a single line of code.
Taking advantage of shorthand syntax allows more than one variable declaration in a single line of code:
a, b := 1, 2
Declaring multiple variables using optional initialization is possible in one line as well:
var a, b, c int
A rune is an alias for int32
that contains a single Unicode code point.
Runes are useful when working with text. An important thing to remember with runes is that they get presented as int32
in code. This means that attempting to display a rune
will show a number instead of the expected Unicode character.
To get the visual representation of a rune
, the %c
formatting token gets
used.
A map is a data structure that stores data in a key/value pair.
Accessing a value in the map requires knowing the key associated with the value you wish to access. Maps use random ordering so they are not appropriate to use when the order of the data is important.
Creating maps requires knowing the types of both the key and value.
Creating a map uses the var
keyword and then a function call to make
must follow to make the map usable:
var m map[string]int
m = make(map[string]int)
Using shorthand initialization syntax rolls this into a single line:
m := make(map[string]int)
We can also create a new map and then populate it with some initial values:
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
To write data to a map, we need to provide the key and the value:
m["key"] = 1
To read data from a map, we need to provide the key:
value := m["key"]
If you aren't sure if the key exists in the map, you can check by attempting to read it:
value, exists := m["key"]
If exists
is true
, then the key is present in the map, and if exists
is
false, then the key was not present in the map. It's important to check this
boolean value because Go will return a default value if the key doesn't exist.
Using the delete
function will delete a key from a map:
delete(m, "key")
delete
doesn't have any return value and doesn't provide the status of
deletion.
If you want to ensure that you delete a key that already exists, you'll need to check if the key exists first by trying to read it, and then run the delete function.
An array is a contiguous chunk of memory.
It can contain a specified number of elements and accessing each element uses an offset to a memory location. Go handles the addressing automatically, so the offset (index) gets used.
To create an uninitialized array in Go with the capacity for 2 elements:
var names [2]string
To create an array with initial values:
names := [2]string{"Alice", "Bob"}
Reading data from an array and writing data to an array both use indexing:
names := [2]string{"Alice", "Bob"}
alice := names[0]
// replace "Bob" with "Carol"
names[1] = "Carol"
fmt.Println(names[1]) // prints "Carol"
fmt.Println(alice) // prints "Alice"
Arrays are a fixed size and cannot get resized once created. Using a
slice
provides a way to "resize" an array by copying the data to an array of
different size.
A slice
is a view into a backing array.
Slices make it more convenient to work with arrays, such as providing the option to resize the slice or perform common operations such as append or pop in an efficient way.
Go has type casting functions to convert between numeric types. For example, to
convert an int
to a float
:
i := 99
var f float32
f = float32(i)
If you've been able to answer a few of the initial set of questions with confidence and ease, the interviewer may move on to more advanced questions about the language.
Their goal here is establish your current skill level.
The more correct answers you can give here, the better your chance at getting hired (or at least moving onto the next stage).
Function literals are also known as anonymous functions, or closures.
Function literals allow you to write a function within a function, or to assign a function to a variable.
Variadic functions are functions which accept a variable number of arguments.
When a variadic function processes the arguments to the function, it does so using a slice. Each argument is an element in the slice, and iterating the slice will produce all arguments passed to the function.
To create a variadic function, use three dots in the function parameters:
func sum(nums ...int) int {
fmt.Println(nums)
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
}
Variadic functions are useful when you don't know the number of inputs that will get provided to the function, or if you want to offer a more convenient API where the developer provides any amount of arguments they want.
iota
Go keyword do, and why would you use it?The iota
keyword offers a way to increment a value when creating constants.
Instead of assigning each constant a value, using the iota
keyword will take
care of this automatically. Using iota
instead of entering numbers by hand
allows the developer to update or add constants without having to worry about
what values get assigned to them.
Receiver functions are functions defined on a specified data type. They enable the use of dot notation when calling functions and provide a way to organize functionality per type.
Receiver functions are also required when implementing an interface.
A pointer is a variable that contains a memory address. The data at the memory address is what the pointer "points" to.
Pointers increase the efficiency of the program. Instead of making multiple copies of data, a pointer instead points to a single copy of the data. The pointer itself can then get copied using minimal resources while still maintaining the ability to access the data it points to.
When a pointer gets dereferenced, the data which exists at the memory address stored in the pointer will get accessed. This allows developers to access data using an indirection: First the pointer gets accessed to discover the memory address where the data exists, and then the data gets accessed using that address.
No, Go does not support pointer arithmetic. Go guarantees memory safety and allowing pointer arithmetic would break this guarantee because manipulating pointers can result in pointing to invalid data. This has the possibility of causing security issues.
A goroutine is a lightweight thread of execution managed by the Go runtime. They execute concurrently with the main thread and their purpose is to enable simplified development of efficient concurrent systems.
Goroutines get spawned using the go
keyword followed by a function call.
A mutex is a mutually exclusive lock.
It provides a way to synchronize access to a piece of data by ensuring that data gets accessed by one goroutine at a time. This prevents undefined behavior because whenever a goroutine writes data, other goroutines have to wait until the write completes before they can read.
A mutex works using a locking system where the lock is either "locked" or "unlocked". When setting a mutex to "locked", no other goroutine can lock it again. If they try to, the operation will block until the first goroutine unlocks it. This locking system prevents any two goroutines from obtaining the lock at the same time.
There are two ways to stop a goroutine:
stop
and resume
Yes, a Go program can terminate at any time, and any goroutine may also terminate the entire program.
When the program ends, any goroutines still running will also end.
Since goroutines are independent, there is nothing stopping the main program from doing whatever it needs to do, and then ending. This means the programmer must track the status of goroutines and design a way to end the program while also allowing goroutines to finish their work (or abandon it, if acceptable).
An interface is a way to declare that some group of behaviors (functions) exist. A type can then implement the interface and provide the implementation for the behaviors expected by the interface. This enables functions to operate on the interface instead of a specified type.
When a function uses an interface as a parameter, the compiler will check all calls to this function and ensure that each type passed to the function implements the interface. This makes it impossible to call functions that weren't implemented on the type, increasing the reliability of Go programs.
A channel is a bidirectional communication pipe. Channel permit data access through "reading" and "writing", or "sending" and "receiving".
Writing data to a channel uses the arrow operator.
The arrow operator "points" to the direction of data flow. So when the channel is in the left operand, data flows from the right operand into the channel:
ch := make(chan int, 1)
ch <- 1
Reading from a channel uses the arrow operator as well. This time, the channel is the right operand:
ch := make(chan int, 1)
ch <- 1
one := <-ch
Channels come in two forms: buffered and unbuffered.
A buffered channel has space for a specified amount of data and as long as the buffer is not full, more data can get written to the channel. Once the buffer fills, then write operations to the channel will block until space becomes available.
An unbuffered channel has space for one piece of data. When the data gets written to an unbuffered channel, the operation will block until the data gets read out of the channel by some other code (such as code running in a goroutine).
Yes, it's possible to read data from a channel after it closes. Channels act as a message queue and preserve all data that exists in the channel as long as the channel is still in scope in some part of the program.
After closing a channel, the remaining messages will stay in the channel until either they get read out or the channel gets dropped.
Type assertions get used when working with interfaces.
An interface hides the underlying type behind an interface, but a type assertion allows the developer to determine what this underlying type is. This is useful in situations where you need access to the original type that implemented an interface.
Here is an example of a type assertion that checks if a variable is a string:
var msg interface{} = "hi"
if s, ok := msg.(string); ok {
fmt.Printf("msg is a string: %s\n", s)
}
A type switch is a type assertion that uses a switch statement. Since it uses switch, multiple cases can check for different types:
var msg interface{} = "hi"
switch msg.(type) {
case string:
fmt.Println("msg is a string")
case int:
fmt.Println("msg is an int")
default:
fmt.Println("msg is something else")
}
Type switches are useful when you are working with an interface and there are more than one possible concrete types that you need to access.
An empty struct is useful if you need to change a map into a set. If you use the key of a map as the critical piece of information, then you can to ignore the value by using an empty struct.
Channels don't always need to transmit useful data. Sometimes, the act of transmitting empty data is enough to trigger an outcome. Empty structs are perfect for this situation because we can write them to the channel and then read them from the other end without worrying about what data should get sent.
nil
get assigned to variables?nil
can get assigned to variables when the variable's type is a pointer.
Since pointers can be nil
, this is OK, but nil
cannot get assigned to a
variable that stores a value.
Copying slices uses the copy
function:
a := []int{1, 2, 3}
b := make([]int, 3)
copy(b, a)
To copy a map, a loop can iterate the entire map and copy the values from one map to the other:
a := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
b := make(map[string]int)
for k, v := range a {
b[k] = v
}
Embedding is a technique that allows one type to exist within another type.
All the methods and fields in the embedded type become accessible at the root of the parent type. More than one type can get embedded into another type, granting access to all the fields and methods of the initial type and the embedded types.
Embedded fields and methods exist at the root level of the type in which they
get embedded. They can get accessed using parent.embeddedField
or
parent.embeddedTypeName.field
.
To create a test in Go, a test file must get created having the same name as
the file under test and appended with _test
. For example, to create a test
file for sample.go
, the test file must have the name sample_test.go
.
The testing
package must get imported in the test file to access the Go
testing framework. Here is an example:
package main
import "testing"
func testSample(t *testing.T) {
t.Errorf("test failed")
}
init()
function and why would you use it?init()
is a special function that gets ran the first time a package gets
imported and is for situations when the package needs some sort of
initialization.
If you get this far, then the interviewer is confident that you know how to write programs in Go.
They will now test your problem-solving ability by asking open-ended questions related to the job or to projects the company has worked on. They may also ask more technical or advanced questions about Go to determine if you understand how the language works at a finer level of detail.
Creating a structure that contains both a mutex and map can serve as a starting point for creating a thread-safe map. Receiver functions created on the map can expose read, write, and delete operations. Each operation must take out a lock on the mutex, which will provide thread-safety.
To gracefully shutdown a program which is processing multiple jobs, there needs to be a way to communicate with the job processors or the goroutine managing the jobs.
To implement this, we will need:
manager
manager
. This channel gets used to
communicate shutdown status. We'll refer to this channel as term
manager
to receive jobs, where manager
is able to stop
receiving jobs on demand, such as closing a channelThe main thread would spawn manager
and connect term
to it. The main thread
would also need to intercept termination signals.
When a termination signal gets received, the main thread sends a message (it can be an empty struct
) on term
. When manager
receives this message, it stops accepting new jobs.
Once all the jobs complete, manager
then sends another signal back on term
.
The main thread will block attempting to read from term
while it waits
for the signal from manager
. Once received, the main thread can end
the program.
Since it isn't safe to have multiple goroutines writing to a single file, the log ingestion process will need to serialize all the events by using a channel.
A single goroutine responsible for reading data from the channel can then write the data serially to the log file.
WaitGroup
that won't terminate?WaitGroup
operates using a counter.
When the counter is greater than zero, calls to .Wait()
will block, and when the counter is zero, .Wait()
will stop blocking.
When used with goroutines, each goroutine spawned should have a corresponding .Add()
which increments the counter. Each goroutine should call .Done()
when they complete, causing the WaitGroup
counter to decrement by one.
The .Done()
call should use defer
at the beginning of the goroutine, ensuring that it gets called regardless of how the goroutine ends.
Loop initialization variables should never get used directly in a goroutine.
When using the loop initialization variable within the loop, it points to the value. This means that using it in a goroutine will cause the value within the goroutine to change. This happens because the variable in the goroutine is pointing to the value that exists in the for
loop outside the goroutine.
If a goroutine requires the value of the loop initialization variable, then make a copy before sending it to a goroutine:
for i := 0; i < 10; i++ {
value := i
go func() {
// use `value` here.
// `i` will change as the loop iterates
}()
}
recover
function?The recover
function prevents a panic from terminating the program.
It's useful when building high-availability servers with minimal crashes. Using
recover
allows the developer to stop the propagation of program termination
and can allow the server to continue running.
Reading data with one goroutine while another is writing to the same memory location results in undefined behavior.
When a goroutine writes to the same memory that another goroutine is reading from, the write may be in any part of memory. This means the reading goroutine might read the first half of the data in one state, and the second half of the data in another state (altered by the goroutine performing the write).
This would lead to data corruption.
Yes, it's possible to read data from a single variable using goroutines.
If any goroutine needs to write data, then the data should get protected with a mutex. If the goroutines solely read data, then a mutex isn't required.
So there you have it. 58 of the most common Golang questions and answers that you might encounter in an interview with a company using Go.
How did you do? Did you get all 58 correct? If so, I'd say you should stop studying and start interviewing!
Didn't get them all? Got tripped up on some? Don't worry about it because I'm here to help.
If you want to fast-track your Golang knowledge and interview prep, and get as much hands-on practice as you can, check out my complete developer’s guide to Golang.
Not only can you follow it from start to finish and work on fundamentals to advanced concepts, but you can also ask questions in the private Discord community.