Skip to main content

Go Interview Questions

Introduction

Go (or Golang) is a statically typed, compiled programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. It was created to improve programming productivity in an era of multicore processors, networked systems, and large codebases. With its simplicity, efficiency, and strong support for concurrency, Go has become increasingly popular for backend development, cloud services, and DevOps.

This guide covers common Go interview questions you might encounter when applying for Go developer positions. We'll explore fundamental concepts, language features, and practical applications to help you prepare for your interviews.

Basic Go Concepts

What are the key features of Go?

Go's key features include:

  • Simplicity: Clean syntax with minimal keywords
  • Compiled Language: Compiles directly to machine code
  • Static Typing: Type checking at compile time
  • Garbage Collection: Automatic memory management
  • Concurrency Support: Goroutines and channels
  • Fast Compilation: Efficient dependency analysis
  • Built-in Testing: Native testing framework
  • Cross-Platform: Supports multiple operating systems

What is the difference between var and := in Go?

In Go, there are two primary ways to declare variables:

go
// Using var
var name string = "John"
var age int = 30

// Using :=
name := "John"
age := 30

Key differences:

  • var can be used inside and outside functions
  • := can only be used inside functions (short variable declaration)
  • := automatically infers the type from the value
  • var requires explicit type declaration unless you provide an initializer

How does Go handle errors?

Go handles errors using return values rather than exceptions. Functions that can fail typically return an error as their last return value:

go
func openFile(filename string) (*os.File, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
return file, nil
}

// Usage
file, err := openFile("example.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
// Use file...

This approach encourages developers to explicitly check and handle errors.

Data Types and Structures

Explain slices in Go. How are they different from arrays?

Arrays and slices are both used to store sequences of elements in Go, but they have important differences:

Arrays:

  • Fixed size defined at declaration
  • Size is part of the type ([5]int and [10]int are different types)
  • Passed by value (copying the entire array)
go
// Array declaration
var numbers [5]int = [5]int{1, 2, 3, 4, 5}

Slices:

  • Dynamic size (can grow and shrink)
  • Reference to an underlying array
  • Passed by reference
  • Have length and capacity properties
go
// Slice declaration
numbers := []int{1, 2, 3, 4, 5}

// Create slice from array
array := [5]int{1, 2, 3, 4, 5}
slice := array[1:4] // Contains 2, 3, 4

// Create slice with make
slice := make([]int, 5, 10) // length 5, capacity 10

How do maps work in Go?

Maps in Go are hash tables that store key-value pairs. They provide fast lookups, inserts, and deletes:

go
// Declaring a map
var scores map[string]int = map[string]int{
"Alice": 98,
"Bob": 87,
"Carol": 92,
}

// Short declaration
scores := map[string]int{
"Alice": 98,
"Bob": 87,
"Carol": 92,
}

// Creating an empty map
scores := make(map[string]int)

// Adding/updating values
scores["Alice"] = 100
scores["Dave"] = 75

// Checking if a key exists
score, exists := scores["Eve"]
if exists {
fmt.Println("Eve's score:", score)
} else {
fmt.Println("Eve not found")
}

// Deleting a key
delete(scores, "Bob")

Maps are not safe for concurrent use without additional synchronization.

Functions and Methods

What are function values and closures in Go?

In Go, functions are first-class citizens, meaning they can be:

  • Assigned to variables
  • Passed as arguments to other functions
  • Returned from other functions

Function Values:

go
func add(a, b int) int {
return a + b
}

func main() {
// Function value
operation := add

// Using the function value
result := operation(5, 3) // result = 8

// Function as argument
applyOperation(5, 3, add)
}

func applyOperation(x, y int, operation func(int, int) int) int {
return operation(x, y)
}

Closures are functions that reference variables from outside their body:

go
func createCounter() func() int {
count := 0
return func() int {
count++
return count
}
}

func main() {
counter := createCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3

// A new counter has its own state
counter2 := createCounter()
fmt.Println(counter2()) // 1
}

What's the difference between functions and methods in Go?

Functions are standalone:

go
func add(a, b int) int {
return a + b
}

Methods are functions associated with a type (receiver):

go
type Rectangle struct {
width, height float64
}

// Method with receiver type Rectangle
func (r Rectangle) Area() float64 {
return r.width * r.height
}

// Usage
func main() {
rect := Rectangle{width: 10, height: 5}
area := rect.Area() // 50
}

Go has two types of receivers:

  1. Value receivers (func (r Rectangle)) - receive a copy of the value
  2. Pointer receivers (func (r *Rectangle)) - receive a pointer to the value, allowing modification
go
// Pointer receiver method that modifies the receiver
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}

Interfaces and Structs

What are interfaces in Go? How are they implemented?

Interfaces in Go define behavior as a set of methods. Types implicitly implement interfaces by implementing their methods - no explicit declaration is needed:

go
// Define an interface
type Shape interface {
Area() float64
Perimeter() float64
}

// Rectangle implements Shape
type Rectangle struct {
width, height float64
}

func (r Rectangle) Area() float64 {
return r.width * r.height
}

func (r Rectangle) Perimeter() float64 {
return 2*r.width + 2*r.height
}

// Circle implements Shape
type Circle struct {
radius float64
}

func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.radius
}

// Function that accepts any Shape
func printShapeInfo(s Shape) {
fmt.Printf("Area: %f, Perimeter: %f
", s.Area(), s.Perimeter())
}

func main() {
r := Rectangle{width: 10, height: 5}
c := Circle{radius: 7}

// Both work because they implement the Shape interface
printShapeInfo(r)
printShapeInfo(c)
}

What is the empty interface (interface{})?

The empty interface interface{} (or any in Go 1.18+) has no methods, so every type implements it. It's used when you need to handle values of unknown type:

go
func printAny(v interface{}) {
fmt.Println(v)
}

func main() {
printAny(42)
printAny("hello")
printAny([]int{1, 2, 3})
}

To use the concrete value, you need type assertions or type switches:

go
// Type assertion
func process(v interface{}) {
str, ok := v.(string)
if ok {
fmt.Println("String value:", str)
} else {
fmt.Println("Not a string")
}
}

// Type switch
func describe(v interface{}) {
switch val := v.(type) {
case string:
fmt.Printf("String of length %d
", len(val))
case int:
fmt.Printf("Integer with value %d
", val)
case []int:
fmt.Printf("Slice of integers with length %d
", len(val))
default:
fmt.Println("Unknown type")
}
}

Concurrency

Explain goroutines. How are they different from threads?

Goroutines are lightweight threads managed by the Go runtime. They allow concurrent execution:

go
func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d ", i)
}
}

func printLetters() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(150 * time.Millisecond)
fmt.Printf("%c ", i)
}
}

func main() {
// Start goroutines
go printNumbers()
go printLetters()

// Wait to see output
time.Sleep(2 * time.Second)
}

Differences between goroutines and OS threads:

GoroutinesOS Threads
Managed by Go runtimeManaged by OS
Very lightweight (2KB stack)Heavyweight (MB stack)
Can create thousands easilyLimited by system resources
Multiplexed onto OS threadsOne-to-one with kernel resources
Communicates via channelsCommunicates via shared memory and locks
Faster startup/teardownMore overhead for creation/destruction

What are channels in Go? How do they work?

Channels are a typed conduit for sending and receiving values between goroutines:

go
// Create an unbuffered channel
ch := make(chan int)

// Create a buffered channel with capacity 3
bufferedCh := make(chan string, 3)

Using channels:

go
func calculateSum(numbers []int, resultCh chan int) {
sum := 0
for _, num := range numbers {
sum += num
}
resultCh <- sum // Send result to channel
}

func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// Split the work
ch := make(chan int)
go calculateSum(numbers[:len(numbers)/2], ch)
go calculateSum(numbers[len(numbers)/2:], ch)

// Receive results
sum1 := <-ch
sum2 := <-ch

fmt.Println("Total sum:", sum1 + sum2)
}

Buffered vs. Unbuffered Channels:

  • Unbuffered channels block the sender until the receiver is ready
  • Buffered channels only block when the buffer is full

Directional Channels:

go
func send(ch chan<- int) {  // Send-only channel
ch <- 42
}

func receive(ch <-chan int) { // Receive-only channel
val := <-ch
fmt.Println(val)
}

Closing Channels:

go
close(ch)  // Close a channel

// Check if a channel is closed
val, ok := <-ch
if !ok {
fmt.Println("Channel closed")
}

// Range over channel until closed
for val := range ch {
fmt.Println(val)
}

What is the select statement in Go?

The select statement lets you wait on multiple channel operations:

go
func main() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(1 * time.Second)
ch1 <- "message from channel 1"
}()

go func() {
time.Sleep(2 * time.Second)
ch2 <- "message from channel 2"
}()

// Wait on multiple channels
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout!")
return
}
}
}

Key features:

  • If multiple cases are ready, one is chosen randomly
  • Can include a default case for non-blocking operations
  • Can include timeouts using the time.After function

Memory Management and Performance

How does garbage collection work in Go?

Go uses a concurrent mark-and-sweep garbage collector with several optimizations:

  1. Concurrent: The GC runs concurrently with the program
  2. Non-generational: Treats all objects equally, not divided by age
  3. Tri-color algorithm: Uses a three-color marking system
  4. Write barriers: Tracks pointer updates during collection
  5. Stop-the-world phases: Brief pauses during critical phases

To minimize GC impact:

  • Reduce allocations by reusing objects
  • Use stack allocations where possible
  • Preallocate slices and maps with expected capacity
  • Consider object pooling for frequently created/destroyed objects

What are defer, panic, and recover in Go?

defer: Schedules a function call to be executed just before the function returns:

go
func processFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Will be executed when function returns

// Process file...
return nil
}

Deferred calls are executed in LIFO (last-in-first-out) order.

panic: Stops normal execution flow and begins panicking:

go
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}

recover: Regains control after a panic:

go
func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()

// Operation that might panic
performRiskyOperation()

return nil
}

Typical use is to recover from unexpected panics in a controlled way.

Common Go Patterns

What is the Go way of handling errors?

Go encourages explicit error handling through return values:

go
// Good Go error handling
func processData(data []byte) (Result, error) {
if len(data) == 0 {
return Result{}, errors.New("empty data")
}

// Process data...
return result, nil
}

// Usage
result, err := processData(data)
if err != nil {
// Handle error
log.Printf("Error processing data: %v", err)
return
}
// Use result...

Best practices:

  1. Return errors rather than using panic
  2. Check errors immediately
  3. Wrap errors to add context: fmt.Errorf("failed to process file: %w", err)
  4. Create custom error types for specific errors
  5. Use sentinel errors for specific conditions: if errors.Is(err, io.EOF) { ... }

What are some common concurrency patterns in Go?

Worker Pool Pattern:

go
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d
", id, job)
time.Sleep(time.Second) // Simulate work
results <- job * 2
}
}

func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)

// Start workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}

// Send jobs
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)

// Collect results
for a := 1; a <= 9; a++ {
<-results
}
}

Fan-out, Fan-in Pattern:

go
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}

func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}

func merge(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup

// Start an output goroutine for each input channel
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}

wg.Add(len(cs))
for _, c := range cs {
go output(c)
}

// Close once all input channels have been drained
go func() {
wg.Wait()
close(out)
}()

return out
}

func main() {
in := generator(1, 2, 3, 4, 5)

// Fan out - distribute work across multiple goroutines
c1 := square(in)
c2 := square(in)

// Fan in - merge channels
for n := range merge(c1, c2) {
fmt.Println(n)
}
}

Testing in Go

How do you write tests in Go?

Go includes a built-in testing package. Tests are functions in files with names ending in _test.go:

go
// math.go
package math

func Add(a, b int) int {
return a + b
}
go
// math_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
// Test cases
testCases := []struct {
a, b int
expected int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
{5, -5, 0},
}

for _, tc := range testCases {
result := Add(tc.a, tc.b)
if result != tc.expected {
t.Errorf("Add(%d, %d) = %d; expected %d",
tc.a, tc.b, result, tc.expected)
}
}
}

Run tests with:

go test ./...

What are benchmarks and how do you write them?

Benchmarks measure performance using the testing package:

go
// math_test.go
func BenchmarkAdd(b *testing.B) {
// Run the Add function b.N times
for i := 0; i < b.N; i++ {
Add(10, 20)
}
}

Run benchmarks with:

go test -bench=.

Example output:

BenchmarkAdd-8      2000000000    0.33 ns/op

Advanced Go Topics

What is the context package and how is it used?

The context package provides a way to carry deadlines, cancellation signals, and request-scoped values across API boundaries:

go
func fetchData(ctx context.Context, url string) ([]byte, error) {
// Create HTTP request with context
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}

// Execute request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

// Check if context was canceled during execution
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
// Context still valid, continue
}

return ioutil.ReadAll(resp.Body)
}

func main() {
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

data, err := fetchData(ctx, "https://api.example.com/data")
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Request timed out")
} else {
fmt.Println("Error:", err)
}
return
}

fmt.Printf("Received %d bytes
", len(data))
}

Common context functions:

  • context.Background(): Root context
  • context.TODO(): Placeholder when unsure which context to use
  • context.WithCancel(): Adds cancellation
  • context.WithDeadline(): Adds absolute deadline
  • context.WithTimeout(): Adds relative timeout
  • context.WithValue(): Adds key-value pair

What are Go modules and how do they work?

Go modules are the official dependency management system for Go (introduced in Go 1.11):

// Basic module commands
go mod init github.com/username/myproject // Initialize a new module
go get github.com/[email protected] // Add dependency at specific version
go get -u // Update all dependencies
go mod tidy // Clean up dependencies
go mod vendor // Copy dependencies to vendor/

Key files:

  • go.mod: Defines the module path and dependencies
  • go.sum: Contains cryptographic hashes of dependencies

Example go.mod file:

module github.com/username/myproject

go 1.18

require (
github.com/gin-gonic/gin v1.8.1
github.com/go-sql-driver/mysql v1.6.0
)

replace github.com/old/package => github.com/new/package v1.0.0

Benefits of modules:

  • Reproducible builds
  • Semantic versioning
  • Dependency isolation
  • Direct and indirect dependency management

Practical Go Interview Problems

Problem: Implement a concurrent web scraper

go
package main

import (
"fmt"
"log"
"net/http"
"sync"
"time"

"golang.org/x/net/html"
)

// Result represents a page scraping result
type Result struct {
URL string
Title string
Error error
}

// Scrape a single URL and return its title
func scrapeURL(url string) Result {
resp, err := http.Get(url)
if err != nil {
return Result{URL: url, Error: err}
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return Result{
URL: url,
Error: fmt.Errorf("status code: %d", resp.StatusCode),
}
}

doc, err := html.Parse(resp.Body)
if err != nil {
return Result{URL: url, Error: err}
}

// Extract title
title := ""
var findTitle func(*html.Node)
findTitle = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil {
title = n.FirstChild.Data
return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
findTitle(c)
}
}
findTitle(doc)

return Result{URL: url, Title: title}
}

func main() {
urls := []string{
"https://golang.org",
"https://blog.golang.org",
"https://pkg.go.dev",
"https://github.com/golang",
}

// Create a channel for results
results := make(chan Result)

// WaitGroup to keep track of goroutines
var wg sync.WaitGroup

// Launch goroutines to scrape URLs
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
// Add timeout for each request
time.Sleep(100 * time.Millisecond) // Simulate work
results <- scrapeURL(url)
}(url)
}

// Close results channel when all goroutines complete
go func() {
wg.Wait()
close(results)
}()

// Process results as they arrive
for result := range results {
if result.Error != nil {
log.Printf("Error scraping %s: %v
", result.URL, result.Error)
} else {
fmt.Printf("Title of %s: %s
", result.URL, result.Title)
}
}
}

Problem: Implement a rate limiter

go
package main

import (
"fmt"
"sync"
"time"
)

// RateLimiter limits the rate of actions
type RateLimiter struct {
rate int // Number of tokens per interval
interval time.Duration // Interval for token refresh
tokens int // Current number of tokens
lastTime time.Time // Last time tokens were added
mu sync.Mutex // Mutex for thread safety
}

// NewRateLimiter creates a new rate limiter
func NewRateLimiter(rate int, interval time.Duration) *RateLimiter {
return &RateLimiter{
rate: rate,
interval: interval,
tokens: rate,
lastTime: time.Now(),
}
}

// Allow checks if an action is allowed
func (rl *RateLimiter) Allow() bool {
rl.mu.Lock()
defer rl.mu.Unlock()

// Refill tokens based on elapsed time
now := time.Now()
elapsed := now.Sub(rl.lastTime)

// Calculate tokens to add based on elapsed time
tokensToAdd := int(elapsed / rl.interval) * rl.rate
if tokensToAdd > 0 {
rl.tokens = min(rl.rate, rl.tokens+tokensToAdd)
rl.lastTime = now
}

// Check if we have a token
if rl.tokens > 0 {
rl.tokens--
return true
}

return false
}

// min returns the smaller of x or y
func min(x, y int) int {
if x < y {
return x
}
return y
}

func main() {
// Rate limit: 3 operations per second
limiter := NewRateLimiter(3, time.Second)

var wg sync.WaitGroup
for i := 1; i <= 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
if limiter.Allow() {
fmt.Printf("Request %d allowed at %s
", id, time.Now().Format("15:04:05.000"))
} else {
fmt.Printf("Request %d denied at %s
", id, time.Now().Format("15:04:05.000"))
}
}(i)

// Small delay to make output clearer
time.Sleep(200 * time.Millisecond)
}

wg.Wait()
}

Common Mistakes in Go

  1. Goroutine leaks: Not ensuring goroutines terminate

    go
    // Bad: Leaking goroutine
    go func() {
    for {
    // Process forever without exit condition
    }
    }()

    // Good: Provides a way to stop
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go func() {
    for {
    select {
    case <-ctx.Done():
    return
    default:
    // Process work
    }
    }
    }()
  2. Not checking errors

    go
    // Bad: Ignoring errors
    file, _ := os.Open("file.txt")

    // Good: Check and handle errors
    file, err := os.Open("file.txt")
    if err != nil {
    log.Fatalf("Failed to open file: %v", err)
    }
  3. Misusing goroutines

    go
    // Bad: Race condition
    counter := 0
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
    defer wg.Done()
    counter++ // Concurrent access without synchronization
    }()
    }
    wg.Wait()

    // Good: Using mutex
    counter := 0
    var wg sync.WaitGroup
    var mu sync.Mutex
    for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
    defer wg.Done()
    mu.Lock()
    counter++
    mu.Unlock()
    }()
    }
    wg.Wait()
  4. Incorrect variable capture in closures

    go
    // Bad: All goroutines will print the same i
    for i := 0; i < 5; i++ {
    go func() {
    fmt.Println(i) // Will likely print 5 five times
    }()
    }

    // Good: Pass the variable as parameter
    for i := 0; i < 5; i++ {
    go func(val int) {
    fmt.Println(val) // Prints 0, 1, 2, 3, 4 (in some order)
    }(i)
    }

Summary

Go is a modern, high-performance language designed for simplicity, concurrency, and efficiency. Its clean syntax, fast compilation, and powerful concurrency model make it ideal for building scalable applications, particularly in distributed systems and cloud computing.

Key strengths:

  • Excellent standard library
  • Built-in concurrency primitives (goroutines and channels)
  • Fast compilation and execution
  • Static typing with clean, readable syntax
  • Strong support for networking and HTTP services

When preparing for Go interviews, focus on:

  • Understanding Go's core concepts and design philosophy
  • Mastering concurrency patterns
  • Being familiar with common Go libraries and tools
  • Understanding best practices and common pitfalls
  • Practicing algorithmic problem-solving using Go's features

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)