Go Programming Language: 10 Interview Questions With Tested Answers

The Go programming language is small, so ten questions cover most of what an interviewer asks. Each answer below is short, with one tiny program and its real output. I ran every program twice on Go 1.27.1, and both runs printed the same lines.

Gouache painting of ten small terracotta pots of young herbs in a row on a potting bench, one pot vermilion.

What Is a Zero Value in Go?

Every type in the Go programming language has a zero value. It is the value a variable holds when you declare it without setting one. Numbers start at 0, strings at an empty string, booleans at false, and pointers, slices and maps at nil. A struct gets the zero value of each field.

package main

import "fmt"

type Order struct {
	Item  string
	Qty   int
	Paid  bool
	Notes []string
}

func main() {
	var n int
	var s string
	var f float64
	var b bool
	var p *int
	var o Order
	fmt.Printf("%d %q %v %t %v\n", n, s, f, b, p)
	fmt.Printf("%+v\n", o)
	fmt.Println(o.Notes == nil, len(o.Notes))
}

Output:

0 "" 0 false <nil>
{Item: Qty:0 Paid:false Notes:[]}
true 0

The nil slice in the struct is safe to use. Its length is 0, and append works on it. Good Go types make the zero value useful, which is why a sync.Mutex works with no setup.

What Is the Difference Between an Array and a Slice?

An array has a fixed size, and the size is part of its type. Assigning an array copies every element. A slice is a small window onto an array: a pointer, a length and a capacity. Copying a slice copies the window, not the data.

package main

import "fmt"

func main() {
	arr := [3]int{1, 2, 3}
	cp := arr
	cp[0] = 99
	fmt.Println("arrays:", arr, cp)

	a := []int{1, 2, 3, 4}
	b := a[:2]
	b[0] = 99
	fmt.Println("shared:", a, b)

	b = append(b, 7)
	fmt.Println("append inside capacity:", a, b, len(b), cap(b))

	c := append(a, 5)
	c[0] = 0
	fmt.Println("append past capacity:", a, c, len(c), cap(c))
}

Output:

arrays: [1 2 3] [99 2 3]
shared: [99 2 3 4] [99 2]
append inside capacity: [99 2 7 4] [99 2 7] 3 4
append past capacity: [99 2 7 4] [0 2 7 4 5] 5 8

The first line shows the copy: changing cp left arr alone. The second shows sharing: changing b[0] also changed a[0]. The third is the classic trap. The slice b has length 2 and capacity 4, so append wrote the 7 into a[2] and overwrote the 3.

In the last line, a was full, so append allocated a new array with capacity 8, and a stayed unchanged. Go doesn’t promise that growth, so never depend on the 8. To stop sharing, copy the elements with the copy function.

How Do You Check Whether a Key Exists in a Map?

A missing key returns the zero value, so you can’t tell it from a stored zero. The comma ok form fixes that. The second value is true when the key exists.

package main

import "fmt"

func main() {
	stock := map[string]int{"apple": 5, "banana": 0}

	fmt.Println(stock["cherry"])

	for _, name := range []string{"banana", "cherry"} {
		n, ok := stock[name]
		fmt.Println(name, n, ok)
	}

	delete(stock, "banana")
	fmt.Println(len(stock), stock)
}

Output:

0
banana 0 true
cherry 0 false
1 map[apple:5]

Banana is stored with 0 and reports true. Cherry is missing and reports false. Reading a nil map works, but writing to one panics, so create maps with make or a literal.

In What Order Do Deferred Calls Run?

The defer statement schedules a call to run when the surrounding function returns. Deferred calls run last in, first out, like a stack of plates. The arguments are evaluated when the defer statement runs, not when the call runs.

package main

import "fmt"

func main() {
	for i := 1; i <= 3; i++ {
		defer fmt.Println("deferred", i)
	}
	x := 10
	defer fmt.Println("x when deferred:", x)
	x = 20
	fmt.Println("x at the end:", x)
}

Output:

x at the end: 20
x when deferred: 10
deferred 3
deferred 2
deferred 1

The loop’s calls ran as 3, 2, 1. The x argument was 10 when deferred, so the output says 10 even though x became 20. Use defer for cleanup that must run on every path, and write it right after you acquire the resource.

How Does Go Handle Errors?

Go has no exceptions for normal failures. A function returns an error as its last value, and the caller checks it. An error is an ordinary value you can compare, wrap and pass around.

package main

import (
	"errors"
	"fmt"
)

var ErrEmpty = errors.New("name is empty")

func greet(name string) (string, error) {
	if name == "" {
		return "", fmt.Errorf("greet: %w", ErrEmpty)
	}
	return "Hello, " + name, nil
}

func main() {
	for _, name := range []string{"Asha", ""} {
		msg, err := greet(name)
		if err != nil {
			fmt.Println("error:", err)
			fmt.Println("is ErrEmpty:", errors.Is(err, ErrEmpty))
			continue
		}
		fmt.Println(msg)
	}
}

Output:

Hello, Asha
error: greet: name is empty
is ErrEmpty: true

The fmt.Errorf function with %w wraps the original error and adds context. The errors.Is function finds the original through the wrapping, so callers match a sentinel such as ErrEmpty without comparing text.

You could say the repeated if err != nil checks are noisy. Fair point. They are also honest: every failure path is visible, and nothing jumps out of a function unseen. Keep panic for bugs, not for bad input.

How Do You Wait for Goroutines to Finish?

A goroutine is a function that runs concurrently, started with the go keyword. The main function doesn’t wait for goroutines, so the program exits when main returns.

Use sync.WaitGroup to wait. Call Add before starting each goroutine, Done when it finishes, and Wait to block until all are done.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	squares := make([]int, 5)
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			squares[n] = n * n
		}(i)
	}
	wg.Wait()
	fmt.Println(squares)
}

Output:

[0 1 4 9 16]

Each goroutine writes only its own slot, so no lock is needed, and the result is the same every run. I pass i as an argument so each goroutine gets its own copy.

What Is the Difference Between a Buffered and an Unbuffered Channel?

A channel passes values between goroutines. An unbuffered channel has no storage, so a send waits for a receiver. A buffered channel holds a set number of values, and a send waits only when it is full.

package main

import "fmt"

func main() {
	unbuffered := make(chan string)
	go func() { unbuffered <- "handed over" }()
	fmt.Println(<-unbuffered)

	buffered := make(chan int, 2)
	buffered <- 1
	buffered <- 2
	fmt.Println(len(buffered), cap(buffered))

	select {
	case buffered <- 3:
		fmt.Println("third value sent")
	default:
		fmt.Println("buffer full, a send would block")
	}

	close(buffered)
	for v := range buffered {
		fmt.Println("received", v)
	}
}

Output:

handed over
2 2
buffer full, a send would block
received 1
received 2

The first line shows the handoff. The buffer then holds two values with no receiver waiting. The select with a default case tries a third send and takes the default, because a real send would block.

Closing a channel lets range read what is left and stop. Only the sender should close it, and never twice.

How Do You Put a Timeout on a Channel Operation?

Use select with time.After. The select statement runs the first channel operation that is ready. The time.After function returns a channel that delivers after the given time.

package main

import (
	"fmt"
	"time"
)

func wait(ch chan string, limit time.Duration) {
	select {
	case msg := <-ch:
		fmt.Println("got:", msg)
	case <-time.After(limit):
		fmt.Println("timeout after", limit)
	}
}

func main() {
	slow := make(chan string, 1)
	go func() {
		time.Sleep(300 * time.Millisecond)
		slow <- "late result"
	}()
	wait(slow, 50*time.Millisecond)

	ready := make(chan string, 1)
	ready <- "early result"
	wait(ready, 50*time.Millisecond)
}

Output:

timeout after 50ms
got: early result

The slow goroutine needs 300 milliseconds, so the 50 millisecond timeout wins. The second channel already holds a value, so its case runs. For real work, a context with a deadline does the same job and also cancels the calls below it.

How Does a Type Satisfy an Interface in Go?

An interface is a set of method signatures, and a type satisfies it by having those methods. There is no implements keyword, so the code that uses an interface can define it, even for types written elsewhere.

package main

import "fmt"

type Shape interface {
	Area() int
}

type Square struct{ Side int }
type Rect struct{ W, H int }

func (s Square) Area() int { return s.Side * s.Side }
func (r Rect) Area() int   { return r.W * r.H }

func main() {
	shapes := []Shape{Square{3}, Rect{2, 5}}
	total := 0
	for _, s := range shapes {
		fmt.Printf("%T area %d\n", s, s.Area())
		total += s.Area()
	}
	fmt.Println("total", total)
}

Output:

main.Square area 9
main.Rect area 10
total 19

Neither Square nor Rect mentions Shape, yet both go into a slice of Shape. The %T verb prints the real type behind each value. If a type misses a method, the compiler rejects it. Keep interfaces small, and return concrete types.

What Is a Data Race and How Do You Prevent It?

A data race happens when two goroutines use the same variable at once and at least one writes. The classic case is count++ from many goroutines. It reads, adds and writes in three steps, so two goroutines can read the same value and lose an increment. The total changes from run to run.

Go has a race detector, switched on with the -race flag. I did not run it on the test machine, so I show no report. The fix is to let one goroutine at a time change the counter. A sync.Mutex does that: Lock before the change, Unlock after.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	var wg sync.WaitGroup
	count := 0
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			mu.Lock()
			count++
			mu.Unlock()
		}()
	}
	wg.Wait()
	fmt.Println("count:", count)
}

Output:

count: 1000

All 1,000 increments are counted, every time. A channel works too: send each change to one goroutine that owns the counter.

A Short Checklist

Don’t memorize these answers. Run each program, change one line, and predict the output before you run it again. Change the capacity in the slice program. Remove the wg.Wait line. Make the timeout longer than the sleep. Each change teaches the rule better than the sentence does.

When you write Go at work, three habits cover most of these questions. Check every error. Defer cleanup right after you acquire a resource. Never share a variable between goroutines without a lock or a channel. Add the race detector to your test runs when your setup supports it.

Knowing the Go programming language is not memorizing answers, it is predicting what a small program prints.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Best Practices, Go, SQL Scripts
Previous Post
SQL SERVER – Convert Cursor to Set Based Insert
Next Post
Trace Flags 1117 and 1118 No Longer Required since SQL Server 2016

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.