Top 15 Golang Interview Questions

Author: Henryk Konsek
18 min read
Top 15 Golang Interview Questions

Most code-sharing applications that companies use these days for interviews support Golang. Despite different opinions about Go’s characteristics and feasibility of its use, this programming language already gained attention from world-known enterprises. Among them include such giants as Google, Uber and many more. It’s only a few examples of success using Go. Go is already one of the most in-demand languages, and interest will only increase. Due to its simplicity and scalability, a lot of companies consider using it. Among these, you can see both yesterday’s startups and large enterprises like Google. So, in this article, you'll find 15 Golang Interview Questions with explanations that will undoubtedly help you land a job.

1. What is the difference between the = and := operator?

Operator = is the assignment operator. It is used the same way you would use it in any other language.

var greeting string = "hello world"

Operator := provides a syntax of the short variable declarations clause and use for declaration, assignment, and for redeclaration. The type is not necessary because the Go compiler is able to infer the type based on the literal value you assign to the variable.

greeting := "hello world"

2. Can you return multiple values from a function?

Go is capable of returning multiple values from a function. This feature is often used to return an error value along with the result:

value, err := getValue()

...or a boolean to indicate success:

value, hasValue := getValue()

Here is an example of a definition of a function returning two values:

func person() (string, string) {
    return "john", "wayne"
}

func main() {
    name, surname := person()
}

The (string, string) return type in this function signature indicates that the function returns 2 strings. Then we read the 2 different return values from the call with multiple assignment operator.

3. What are function closures?

Go supports anonymous functions, which can form closures. Anonymous functions are useful when you want to define a function inline without having to name it. Anonymous function together with an outside variable it references is known as a closure.

import "fmt"

func main() {
    counter := 0
    add := func(a, b int) int {
        counter++ // This reference makes function a closure
        fmt.Printf("Adder was invoked %d times.", counter)
        return a + b
    }
    sum := add(1, 1)
    sum := add(2, 3)
}

4. Does Go have exceptions?

Go language supports multiple function return values. Error handling by returning error codes from functions is an idiomatic way to indicate an error during an execution. By convention error is the last value returned from a function.

import "fmt"

func sayHello(name string) (string, error) {
    if name == "" {
        return "", errors.New("name cannot be empty")
    }
    return fmt.Sprintf("Hello %s!", name)
}

func main() {
    helloString, err := sayHello("john")
    if err != nil {
        panic(err)
    }
    fmt.Println(helloString)
}

5. Is Go an object-oriented language?

This is a little complex to say yes or no. Object-oriented programming is a programming paradigm which uses the idea of “objects” to represent data and methods. Go does not strictly support object orientation but is a lightweight object Oriented language.

Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. By treating objects of different types in a consistent way, as long as they stick to one interface, Golang implements polymorphism. There are also ways to embed types in other types to provide something analogous but not identical to subclassing.

Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes) and Structs in Golang are user-defined types that hold just the state and not the behavior. In Go, encapsulation is implemented by capitalizing fields, methods, and functions which makes them public.

So basically , Go was not designed to be primarily an object-oriented language in the way other languages are designed but it satisfies the majority of its characteristics.

6. Does Go support method overloading?

Go does not support overloading of methods and operators because method dispatch is simplified if it doesn't need to do type matching as well. Experience with other languages told us that having a variety of methods with the same name but different signatures was occasionally useful but that it could also be confusing and fragile in practice. Matching only by name and requiring consistency in the types was a major simplifying decision in Go's type system.

Therefore ,function overloading was intentionally left out, because it makes code hard(er) to read. We can workaround method/function overloading in Go using:

  • Variadic Function – A Variadic Function is a function that accepts a variable number of arguments.

    func main() {
        fmt.Println(add(1, 2))
    }
    
    func add(price ...int) int {
        total := 0
        for _, value := range price {
            total += value
        }
        return total
    }
  • Empty Interface – It is an interface without any methods.

    func Child(child interface{}) {
    }
    
    func main() {
    }

By using an empty interface as our parameter we have a one parameter function that can take in any type.

7. How do we perform inheritance with Golang?

When an anonymous type is embedded in a struct, the visible methods of that type are embedded as well—in effect, the outer type inherits the methods: to subtype something, you put the parent type within the subtype.This mechanism offers a simple way to emulate some of the effects of subclassing and inheritance found in classic OO-languages;

type Teacher struct {
    teacher1, teacher2 int
}
type Student struct {
    Teacher
    student1, student2 float32
}

func main() {
    output := Student{Teacher{1, 2}, 3.0, 4.0}
    fmt.Println(output.teacher1, output.teacher2, output.student1, output.student2)
    fmt.Println(output.Teacher)
}

In Go types are basically classes (data and associated methods). Go doesn’t know inheritance like class oriented OO languages. Inheritance has two main benefits: code reuse and polymorphism. Code reuse in Go is achieved through composition(In this, base structs can be embedded into a child struct and the methods of the base struct can be directly called on the child struct as shown in the above example) and delegation, and polymorphism through the use of interfaces: it implements what is sometimes called component programming.Go’s interfaces provide a more powerful and yet simpler polymorphic behaviour than class inheritance.

8. Explain Go interfaces. What are they and how do they work?

Interfaces are types that just declare behavior. When a user-defined type implements the set of methods declared by an interface type, values of the user-defined type can be assigned to values of the interface type. This assignment stores the value of the user-defined type into the interface value.

An interface defines a set of methods, but these methods do not contain code: they are not implemented ( they are abstract). Also an interface cannot contain variables. They usually have from 0—max 3 methods.

An interface is declared in the format:

type Person interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    // …
}

where Person is an interface type.

Interface values in memory are two-word data structures:

  • The first word contains a pointer to an internal table called an iTable, which contains type information about the stored value. The iTable contains the type of value that has been stored and a list of methods associated with the value.

  • The second word is a pointer to the stored value. The combination of type information and pointer binds the relationship between the two values

    type Shape interface {
        Area() float32
    }
    type Square struct {
        side float32
    }
    
    func (s *Square) Area() float32 {
        return s.side * s.side
    }
    func main() {
        s1 := new(Square)
        s1.side = 10
        area_Square := s1
        fmt.Printf("The square has area: %f\n", area_Square.Area())
    }

The program defines a struct Square and an interface Shape, with one method Area(). In main() an instance of Square is constructed. Outside of main we have an Area() method with a receiver type of Square where the area of a square is calculated: the struct Square implements the interface Shape.

9. How to compare two interfaces in Go?

An interface variable is represented by a type and value. Two interface variable can be compared using == or != operators.

Interfaces are comparable if either

  • Interface value is nil or

  • The underlying type is the same and comparable. Underlying Value is also the same

Now the runtime will only panic if they both contain the same non-comparable type. (If they contain different types then the result is now false even if either type is non-comparable. Basically, they are slices, maps, functions and any struct or array type that uses them.)

type study interface {
    section1()
    section2()
}
type School struct {
    class int
}

func (S School) section1() {
    fmt.Println("children")
}
func (S School) section2() {
    fmt.Println("parents")
}
func main() {
    var a, b, c study
    a = School{class: 1}
    b = School{class: 1}
    c = School{class: 2}
    if a == b {
        fmt.Println("a and b are equal")
    } else {
        fmt.Println("a and b are not equal")
    }
    if a == c {
        fmt.Println("a and c are equal")
    } else {
        fmt.Println("a and c are not equal")
    }
}

Interface variable a and b are equal because type and value are the same and we know that Interfaces are comparable if the underlying type is the same and comparable and Underlying Value is also the same. But variable a and c are not equal because Underlying value is not the same.

10. What is a goroutine, and why do we use them instead of threads?

A goroutine is a function that is capable of running concurrently with other functions. They are lightweight and we can easily create thousands of them. To create a goroutine we use the keyword go followed by a function invocation:

func f(n int) {
    for i := 0; i < 10; i++ {
        fmt.Println(n, ":", i)
    }
}
func main() {
    go f(0)
    var input string
    fmt.Scanln(&input)
}

This program consists of two goroutines:

  • The first goroutine is implicit and is the main function itself.

  • The second goroutine is created when we call go f(0).

Normally when we invoke a function our program will execute all the statements in a function and then return to the next line following the invocation. With a goroutine we return immediately to the next line and don't wait for the function to complete.

Goroutine instead of threads:

Goroutines are part of making concurrency easy to use. The idea, which has been around for a while, is to multiplex independently executing functions—coroutines—onto a set of threads. When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won't be blocked. The programmer sees none of this, which is the point. The result, which we call goroutines, can be very cheap: they have little overhead beyond the memory for the stack, which is just a few kilobytes.

To make the stacks small, Go's run-time uses resizable, bounded stacks. A newly minted goroutine is given a few kilobytes, which is almost always enough. When it isn't, the run-time grows (and shrinks) the memory for storing the stack automatically, allowing many goroutines to live in a modest amount of memory. The CPU overhead averages about three cheap instructions per function call. It is practical to create hundreds of thousands of goroutines in the same address space. If goroutines were just threads, system resources would run out at a much smaller number.

11. What is a pointer and when would you use it in Go?

Pointers reference a location in memory where a value is stored rather than the value itself (they point to something else). A newly declared pointer which has not been assigned to a variable has the nil value. A pointer variable is often abbreviated as ptr. . Pointed variables also persist in memory, for as long as there is at least 1 pointer pointing to them, so their lifetime is independent of the scope in which they were created. Pointers can also point to other pointers, and this nesting can go arbitrarily deep, so you can have multiple levels of indirection,

func zero(myPointer *int) {
    *myPointer = 0
}
func main() {
    x := 5
    zero(&x)
    fmt.Println(x) // x is 0
}

In Go a pointer is represented using the (asterisk) character followed by the type of the stored value. In the zero function myPointer is a pointer to an int. By using a pointer (*int) the zero function is able to modify the original variable.

Pointers are rarely used with Go's built-in types, but as we know that they are extremely useful when paired with structs.

12 What is shadowing?

A variable is said to be shadowing another variable if it “overrides” the variable in a more specific scope. Shadowed variables are a common form of error that can be quickly identified

Shadowing a variable by misusing short declaration:

var remember bool = false
if something {
    remember := true // Wrong
}

In the previous code-snippet the variable remember will never become true outside of the if-body.

If something is true, inside the if-body a new remember variable which hides the outer remember is declared because of :=, and there it will be true. But after the closing } of if remember regains its outer value false. So write it as:

if something {
    remember = true
}

This can also occur with a for-loop, and can be particularly subtle in functions with named return variables, as the following snippet shows:

func shadow() (err error) {
    x, err := check1() // x is created; err is assigned to
    if err != nil {
        return // err correctly returned
    }
    if y, err := check2(x); err != nil { // y and inner err are created
        return // inner err shadows outer err so nil is wrongly returned!
    } else {
        fmt.Println(y)
    }
    return
}

In the shadow() function’s first statement the x variable is created and assigned to, but the err variable is simply assigned to since it is already declared as the shadow() function’s return value. This works because the := operator must create at least one non blank variable and that condition is met here. So, if err is not nil, it is correctly returned.

So, both the y and the err variables are created, the latter being a shadow variable. If the err is not nil the err in the outer scope is returned (i.e., the err declared as the shadow() function’s return value), which is nil since that was the value assigned to it by the call to check1(), whereas the call to check2() was assigned to the shadowing inner err.

Fortunately, this function’s shadow problem is merely a phantom, since the Go compiler will stop with an error message if we use a bare return when any of the return variables has been shadowed. So, this function will not compile as it stands.

13. What is the procedure for switching from GoDep to GoModules?

Go projects use a wide variety of dependency management strategies. Vendoring tools such as Godep and glide are popular, but they have wide differences in behavior and don't always work well together. Some projects store their entire GOPATH directory in a single Git repository. Others simply rely on go get and expect fairly recent versions of dependencies to be installed in GOPATH.

Go's module system, provides an official dependency management solution built into the go command. Switching from Dep to Go Modules is very easy:

  1. Run go version and make sure you're using Go version 1.11 or later.
  2. Move your code outside of GOPATH or set export GO111MODULE=on.
  3. go mod init will import dependencies from Gopkg.lock.
  4. go mod tidy: This will remove unnecessary imports, and add indirect ones.
  5. (Optional) Delete your vendor folder (rm -rf vendor/ or move to trash)
  6. go build: Do a test build to see if it works.
  7. rm -f Gopkg.lock Gopkg.toml: Delete the obsolete files used for Dep.

Go has imported my dependencies from Dep by reading the Gopkg.lock file and also created a go.mod file. If you want to keep your vendor folder:

  1. Run go mod vendor to copy your dependencies into the vendor folder.
  2. Run go build -mod=vendor to ensure go build uses your vendor folder.

14. Explain how sync.Mutex differs from sync.RWMutex.

Package sync provides basic synchronization primitives such as mutual exclusion locks.

A Mutex is a mutual exclusion lock. The zero value for a Mutex is an unlocked mutex. A Mutex must not be copied after first use. One way to synchronize access to a shared resource is by using a mutex. A mutex is named after the concept of mutual exclusion. A mutex is used to create a critical section around code that ensures only one goroutine at a time can execute that code section. We can also use a mutex to fix the race condition we created.

var (
    mutex   sync.Mutex
    balance int
)

func init() {
    balance = 1000
}
func deposit(value int, wg *sync.WaitGroup) {
    mutex.Lock()
    fmt.Printf("Depositing %d to account with balance: %d\n",
               value, balance)
    balance += value
    mutex.Unlock()
    wg.Done()
}
func withdraw(value int, wg *sync.WaitGroup) {
    mutex.Lock()
    fmt.Printf("Withdrawing %d from account with balance: %d\n",
                value, balance)
    balance -= value
    mutex.Unlock()
    wg.Done()
}

func main() {
    fmt.Println("Go Mutex Example")
    var wg sync.WaitGroup
    wg.Add(2)
    go withdraw(700, &wg)
    go deposit(500, &wg)
    wg.Wait()
    fmt.Printf("New Balance %d\n", balance)
}

Within both our deposit() and our withdraw() functions, we have specified the first step should be to acquire the mutex using the mutex.Lock() method. Each of our functions will block until it successfully acquires the Lock. Once successful, it will then proceed to enter it’s critical section in which it reads and subsequently updates the account’s balance. Once each function has performed it’s task, it then proceeds to release the lock by calling the mutex.Unlock() method.

A RWMutex is a reader/writer mutual exclusion lock. The lock can be held by an arbitrary number of readers or a single writer. The zero value for a RWMutex is an unlocked mutex. A RWMutex must not be copied after first use.

If a goroutine holds a RWMutex for reading and another goroutine might call Lock, no goroutine should expect to be able to acquire a read lock until the initial read lock is released. In particular, this prohibits recursive read locking. This is to ensure that the lock eventually becomes available; a blocked Lock call excludes new readers from acquiring the lock.

As with Mutexes, a locked RWMutex is not associated with a particular goroutine. One goroutine may RLock (Lock) a RWMutex and then arrange for another goroutine to RUnlock (Unlock) it.

type DB struct {
    mu sync.RWMutex
    data map[string]string
}

The mu is of type sync.RWMutex. Actually speaking we could have used Mutex package, but we will use RWMutex instead, which stands for ReadWrite Mutex. A mutex does provide a lock method which will prevent accessing the shared memory until the corresponding unlock method is called. RWMutex has a special type of lock called as RLock, which eventually means Read Lock. The way Read Lock works is as follows:

  • There could be n number of Read Locks (without blocking other Read Lock)
  • However, Write Lock can’t be acquired until all Read Locks are released

Note that, RWMutex does also has a lock which is simply mean write lock.

15. What is the relationship between channels and goroutine?

Channels provide a way for two goroutines to communicate with one another and synchronize their execution.

func pinger(c chan string) {
    for i := 0; ; i++ {
        c <- "ping"
    }
}
func printer(c chan string) {
    for {
        msg := <-c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}
func main() {
    var c chan string = make(chan string)
    go pinger(c)
    go printer(c)
    var input string
    fmt.Scanln(&input)
}

This program will print “ping” forever (hit enter to stop it). A channel type is represented with the keyword chan followed by the type of the things that are passed on the channel (in this case we are passing strings. Using a channel like this synchronizes the two goroutines. When pinger attempts to send a message on the channel it will wait until printer is ready to receive the message. (this is known as blocking)

Related posts