Prev Next

Golang / GoLang Basics Interview Questions

1. What is Go and why was it created at Google? 2. What are the key characteristics that make Go different from other popular languages? 3. What are packages in Go and what is special about the 'main' package? 4. What are the different ways to declare variables in Go? 5. What are the fundamental data types in Go? 6. How do constants and iota work in Go? 7. How are functions defined in Go? What are variadic functions and named return values? 8. How do if, for, and switch statements work in Go? 9. What is the difference between arrays and slices in Go? 10. How do maps work in Go? What are the key operations and pitfalls? 11. How do structs work in Go and how do you attach methods to them? 12. How do interfaces work in Go? How do you use type assertions and type switches? 13. What is the empty interface (any / interface{}) and when should you use it? 14. How do pointers work in Go and how are they safer than C pointers? 15. How does Go handle errors, and what is the difference between %v and %w in fmt.Errorf? 16. What are goroutines and how do you use sync.WaitGroup to wait for them? 17. What are channels in Go and what is the difference between buffered and unbuffered? 18. How do defer, panic, and recover work together in Go? 19. What are closures in Go and what is the loop variable capture gotcha? 20. What is the init() function and when does it run? 21. What is the Go module system? What do go.mod and go.sum contain? 22. What is a data race in Go and how do you detect one? 23. What is the fmt.Stringer interface and how does it control how a type is printed? 24. What is the difference between a type definition and a type alias in Go? 25. How does Go handle strings, runes, and bytes? Why is len(s) not the character count? 26. What is a goroutine leak and what is the idiomatic way to prevent one? 27. What is sync.Mutex and when do you use sync.RWMutex instead? 28. How does struct embedding promote methods in Go, and how does it differ from inheritance? 29. How does append() work internally in Go? When does it allocate new memory? 30. What is context.Context and why is it the first parameter in so many Go functions? 31. What is the 'typed nil' trap in Go and why does 'if err != nil' sometimes fail? 32. What are the most important formatting verbs in Go's fmt package?
Could not find what you were looking for? send us the question and we would be happy to answer your question.

1. What is Go and why was it created at Google?

Go (also called Golang) is an open-source, statically typed, compiled programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. It was announced in 2009 and reached version 1.0 in 2012.

The creators were frustrated with the tools available at Google. C++ compile times were painfully slow on massive codebases, Java was verbose and required heavy infrastructure, and dynamically typed languages lacked compile-time safety. They wanted a language that combined the performance of C, the readability of Python, and first-class support for concurrency on multicore hardware.

Go Design Goals
GoalHow Go achieves it
Fast compilationSimple grammar, no header files, dependency graph resolved at compile time β€” even large codebases compile in seconds
Readable codeMinimal syntax, one way to do most things, gofmt enforces consistent formatting
Built-in concurrencyGoroutines and channels are language primitives, not library add-ons
Memory safetyGarbage collector, bounds checking, no manual malloc/free
Simple deploymentgo build produces a single statically linked binary with no runtime dependencies
// A complete Go program
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Go is the language behind Docker, Kubernetes, Terraform, and many cloud-native tools. Companies like Google, Uber, Dropbox, and Cloudflare use it extensively for high-throughput backend services and CLI tooling.

Which company created the Go programming language?
What was one of the main frustrations that motivated Go's creation?
2. What are the key characteristics that make Go different from other popular languages?

Go has a deliberately small feature set. Every design decision was made by asking: does this add enough value to justify the complexity it introduces? The result is a language that experienced developers can learn in days and that reads consistently across large teams.

Go Key Characteristics
CharacteristicDescription
Statically typedTypes checked at compile time β€” type errors are found before the program runs
Compiled to native codeNo VM or interpreter β€” source compiles directly to machine code for fast startup and execution
Garbage collectedMemory managed automatically; Go's concurrent GC has sub-millisecond pause targets
Goroutines & channelsConcurrency primitives built into the language, not bolted on as a library
No classes or inheritanceStructs + interfaces + embedding: composition over inheritance
Implicit interface satisfactionA type implements an interface just by having the required methods β€” no 'implements' keyword
Multiple return valuesFunctions return (result, error) β€” the idiomatic error-handling pattern
Single binary outputgo build produces one statically linked executable β€” trivial to deploy
// Multiple return values β€” idiomatic Go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 3)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%.2f\n", result) // 3.33
Which statement about Go is correct?
What does 'implicit interface satisfaction' mean in Go?
3. What are packages in Go and what is special about the 'main' package?

Every Go source file starts with a package declaration. Packages are Go's unit of code organisation, encapsulation, and compilation. A package groups related types, functions, constants, and variables.

The main package is unique: it defines an executable program. The main() function within it is the program's entry point. Any package not named main is a library package β€” importable by others but not directly runnable.

Exported vs unexported: identifiers starting with an uppercase letter are exported (public). Lowercase identifiers are unexported (package-private). This is Go's entire visibility system β€” no public, private, or protected keywords.

// main.go β€” executable program
package main

import (
    "fmt"
    "myproject/mathutil"   // importing a library package
)

func main() {
    result := mathutil.Add(3, 4)  // Add is exported (uppercase)
    fmt.Println(result)           // 7
}

// mathutil/math.go β€” library package
package mathutil

func Add(a, b int) int { return a + b }    // exported
func helper(x int) int { return x * 2 }    // unexported (private)
What makes the 'main' package special in Go?
How does Go control whether an identifier is accessible from outside its package?
4. What are the different ways to declare variables in Go?

Go offers several declaration styles. The choice between them is mostly about context (package level vs inside a function) and verbosity. Every variable is always initialised β€” Go has no uninitialized variables.

Variable Declaration Styles
StyleWhere usableType required?Notes
var name typeAnywhereYesInitialised to zero value
var name = valueAnywhereNo β€” inferredUseful when type is obvious from value
name := valueFunctions onlyNo β€” inferredMost common inside functions
var (name type; ...)AnywhereYesGroups multiple declarations
// Package-level: var keyword required
var appName string = "MyApp"
var maxRetries = 3           // type inferred as int

// Function-level: short declaration preferred
func main() {
    greeting := "Hello"      // most common β€” := infers type
    x, y := 10, 20           // multiple assignment
    x, y = y, x             // swap without temp variable!

    // Blank identifier: discard unwanted values
    result, _ := divide(10, 3)  // ignore the error

    // var block for related declarations
    var (
        firstName = "Alice"
        lastName  = "Smith"
        age       = 30
    )
    fmt.Println(greeting, result, firstName, lastName, age)
}

// Zero values β€” every variable gets one
var i int     // 0
var f float64 // 0.0
var b bool    // false
var s string  // ""
Which variable declaration style can ONLY be used inside functions in Go?
What is the zero value of a bool variable in Go?
5. What are the fundamental data types in Go?

Go has a concise but complete set of built-in primitive types. Choosing the right type β€” especially between int and sized integers, and between float32 and float64 β€” matters for correctness and interoperability.

Go Primitive Types
CategoryTypesCommon default
Signed integersint8, int16, int32, int64, intint (platform width: 64-bit on 64-bit OS)
Unsigned integersuint8, uint16, uint32, uint64, uintuint8 alias = byte
Floating pointfloat32, float64float64 (more precise; the default)
Booleanboolfalse
Stringstring"" (immutable UTF-8 bytes)
Runerune (= int32)represents a Unicode code point
var i   int     = 42
var f   float64 = 3.14159
var b   byte    = 255        // alias for uint8
var r   rune    = '⚑'       // alias for int32 β€” Unicode code point
var str string  = "Hello, δΈ–η•Œ"

// Type conversions are ALWAYS explicit β€” no implicit casting
var x int = 100
var y float64 = float64(x)   // must be explicit
var z int = int(y)            // truncates decimal part

// String byte count vs character count (UTF-8)
fmt.Println(len(str))             // 13 bytes
fmt.Println(len([]rune(str)))     // 9 runes/characters

// Constants are untyped by default β€” flexible in expressions
const Pi = 3.14159
const MaxItems = 1000
What type is 'byte' an alias for in Go?
Why are type conversions always explicit in Go?
6. How do constants and iota work in Go?

Constants are declared with const and must be assigned a value that is computable at compile time β€” no function calls or runtime values. The iota identifier provides an automatically incrementing integer within a const block, resetting to 0 at the start of each new block.

// Simple constants
const Pi    = 3.14159
const AppName = "MyService"

// iota: auto-incrementing integer, resets at each const block
type Weekday int
const (
    Sunday Weekday = iota  // 0
    Monday                 // 1
    Tuesday                // 2
    Wednesday              // 3
    Thursday               // 4
    Friday                 // 5
    Saturday               // 6
)

// iota with bit-shifting β€” perfect for flag constants
type Permission uint
const (
    Read    Permission = 1 << iota  // 1 (001)
    Write                           // 2 (010)
    Execute                         // 4 (100)
)
userPerms := Read | Write  // 3 β€” can read and write

// iota with expressions
const (
    _  = iota              // skip 0
    KB = 1 << (10 * iota)  // 1 << 10 = 1024
    MB                     // 1 << 20
    GB                     // 1 << 30
)
What value does iota have at the start of each new const block?
What does the blank identifier '_' accomplish when used with iota in a const block?
7. How are functions defined in Go? What are variadic functions and named return values?

Functions are first-class citizens in Go β€” they can be assigned to variables, passed as arguments, and returned from other functions. Go functions support multiple return values (the primary mechanism for error handling), named returns, and variadic parameters.

// Basic function with multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 { return 0, errors.New("division by zero") }
    return a / b, nil
}

// Named return values β€” document what is returned
// Use sparingly; best for short functions only
func minMax(nums []int) (min, max int) {
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min { min = n }
        if n > max { max = n }
    }
    return  // naked return β€” returns named values min and max
}

// Variadic function β€” accepts 0 or more arguments of the given type
func sum(nums ...int) int {
    total := 0
    for _, n := range nums { total += n }
    return total
}
sum(1, 2, 3)        // 6
s := []int{4, 5, 6}
sum(s...)           // 15 β€” spread a slice with ...

// Function as a value (first-class)
double := func(n int) int { return n * 2 }
fmt.Println(double(7))  // 14

// Higher-order function
func apply(nums []int, f func(int) int) []int {
    result := make([]int, len(nums))
    for i, v := range nums { result[i] = f(v) }
    return result
}
fmt.Println(apply([]int{1, 2, 3}, double))  // [2 4 6]
What syntax spreads a slice into a variadic function argument?
What is a 'naked return' in Go?

8. How do if, for, and switch statements work in Go?

Go has a deliberately minimal set of control-flow constructs. There is only one loop keyword β€” for β€” which covers everything a while, do-while, and classic for loop does in other languages.

// if with an init statement β€” err is only in scope inside the if block
if err := doWork(); err != nil {
    log.Fatal(err)
}

// for β€” C-style
for i := 0; i < 5; i++ { fmt.Println(i) }

// for as while
n := 1
for n < 128 { n *= 2 }

// for β€” infinite loop (use break or return to exit)
for {
    if done() { break }
    doWork()
}

// for range β€” slices, maps, strings, channels
fruits := []string{"apple", "banana", "cherry"}
for i, fruit := range fruits {
    fmt.Printf("%d: %s\n", i, fruit)
}
for _, fruit := range fruits { fmt.Println(fruit) }  // ignore index

// switch β€” no implicit fallthrough
switch day {
case "Sat", "Sun":
    fmt.Println("Weekend")
case "Mon", "Tue", "Wed", "Thu", "Fri":
    fmt.Println("Weekday")
default:
    fmt.Println("Unknown")
}

// switch with no expression β€” replaces long if-else chains
switch {
case score >= 90: fmt.Println("A")
case score >= 80: fmt.Println("B")
default:          fmt.Println("C or below")
}
In Go, which single keyword handles the roles of 'for', 'while', and infinite loops?
In Go's switch statement, does execution fall through from one case to the next by default?
9. What is the difference between arrays and slices in Go?

Arrays and slices are both ordered sequences, but they work very differently. Arrays are value types with a fixed size baked into their type; slices are reference types that provide a flexible view into an underlying array.

Array vs Slice
AspectArraySlice
SizeFixed at compile time; part of the type ([5]int β‰  [6]int)Dynamic β€” can grow via append
Passed to functions asFull copy (value type β€” can be expensive)3-word header: pointer + len + cap (cheap)
Zero value[0, 0, 0, ...]nil (len=0, cap=0)
Common in practiceRarely β€” mainly fixed-size buffers or keysThe everyday Go sequence type
// Array
var arr [5]int                       // [0 0 0 0 0]
arr2 := [3]string{"a", "b", "c"}
arr3 := [...]int{1, 2, 3, 4}         // compiler counts: [4]int

// Slice
s := []int{1, 2, 3}                  // slice literal
s2 := make([]int, 5)                 // len=5, cap=5, all zeros
s3 := make([]int, 3, 10)             // len=3, cap=10

// append β€” returns a NEW slice (may reallocate backing array)
s = append(s, 4, 5)                  // [1 2 3 4 5]

// Sub-slice β€” shares backing array!
sub := s[1:4]                        // [2 3 4]
sub[0] = 99                           // changes s[1] too!

// copy β€” independent backing array
dst := make([]int, len(s))
copy(dst, s)

// nil slice vs empty slice
var nilSlice []int                   // nil β€” len=0, cap=0
empty := []int{}                     // not nil β€” len=0, cap=0
fmt.Println(nilSlice == nil)          // true
fmt.Println(empty == nil)             // false
What does a Go slice header contain?
When you create a sub-slice 's2 := s[1:4]', what happens to the underlying array?
10. How do maps work in Go? What are the key operations and pitfalls?

Maps are Go's built-in hash table. Keys must be comparable types (those that support == and !=). Maps are reference types β€” like slices, they are cheap to pass because only a header is copied.

// Creating maps
m := map[string]int{}             // empty map literal (ready to use)
m2 := make(map[string]int)        // same β€” make preferred when initial size is known
m3 := map[string]int{             // initialised map
    "alice": 30, "bob": 25,
}

// Insert / update
m3["carol"] = 28
m3["alice"] = 31                 // update β€” no error if key exists

// Read β€” returns zero value for missing keys, NOT an error
age := m3["alice"]               // 31
missing := m3["dave"]            // 0 β€” zero value for int

// Comma-ok idiom β€” check if key actually exists
age, ok := m3["alice"]
if ok {
    fmt.Printf("alice is %d\n", age)
}

// Delete
delete(m3, "bob")

// Iteration β€” ORDER IS NOT GUARANTEED
for name, age := range m3 {
    fmt.Printf("%s: %d\n", name, age)
}

// PITFALL: reading nil map is OK (returns zero value)
var bad map[string]int
_  = bad["key"]       // safe β€” returns 0
// bad["key"] = 1     // PANIC: assignment to entry in nil map
What does reading a missing key from a Go map return?
Is map iteration order guaranteed in Go?
11. How do structs work in Go and how do you attach methods to them?

Structs are Go's primary mechanism for grouping related data. Methods are functions with a receiver β€” the type they are attached to. The receiver can be a value or a pointer, which changes whether the method can modify the struct.

type Person struct {
    FirstName string
    LastName  string
    Age       int
    email     string   // unexported
}

// Creating struct instances
p1 := Person{FirstName: "Alice", LastName: "Smith", Age: 30}
p2 := &Person{FirstName: "Bob", Age: 25}  // pointer to struct

// Value receiver β€” works on a copy; cannot modify the original
func (p Person) FullName() string {
    return p.FirstName + " " + p.LastName
}

// Pointer receiver β€” modifies the original struct
func (p *Person) HaveBirthday() {
    p.Age++
}

fmt.Println(p1.FullName())   // Alice Smith
p1.HaveBirthday()            // Go auto-takes address: (&p1).HaveBirthday()
fmt.Println(p1.Age)          // 31

// Anonymous struct β€” useful for one-off data grouping
point := struct{ X, Y int }{X: 3, Y: 7}

// Struct embedding β€” promotes fields and methods (composition)
type Employee struct {
    Person               // embedded β€” promotes Name, HaveBirthday, etc.
    Company string
    Salary  float64
}
e := Employee{Person: Person{FirstName: "Carol", Age: 28}, Company: "Acme"}
fmt.Println(e.FullName())    // Carol  β€” promoted method
When should you use a pointer receiver instead of a value receiver for a method?
What does struct embedding in Go achieve?
12. How do interfaces work in Go? How do you use type assertions and type switches?

An interface specifies a set of method signatures. Any type that implements all the methods satisfies the interface β€” implicitly, with no declaration. This is sometimes called structural typing or duck typing with static checking.

// Interface definition
type Shape interface {
    Area()      float64
    Perimeter() float64
}

// Circle satisfies Shape β€” no 'implements' keyword needed
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 }

type Rectangle struct{ W, H float64 }
func (r Rectangle) Area()      float64 { return r.W * r.H }
func (r Rectangle) Perimeter() float64 { return 2 * (r.W + r.H) }

// Polymorphic function β€” accepts any Shape
func printShape(s Shape) {
    fmt.Printf("Area=%.2f Perimeter=%.2f\n", s.Area(), s.Perimeter())
}

shapes := []Shape{Circle{5}, Rectangle{4, 6}}
for _, s := range shapes { printShape(s) }

// Type assertion β€” extract concrete type (safe form)
var s Shape = Circle{Radius: 3}
c, ok := s.(Circle)
if ok { fmt.Println("radius:", c.Radius) }  // radius: 3

// Type switch β€” check and handle multiple types
switch v := s.(type) {
case Circle:    fmt.Printf("circle r=%.1f\n", v.Radius)
case Rectangle: fmt.Printf("rect %.0fx%.0f\n", v.W, v.H)
default:        fmt.Println("unknown shape")
}

// Compile-time interface check (zero-cost assertion)
var _ Shape = Circle{}  // COMPILE ERROR if Circle doesn't implement Shape
What keyword does a Go type use to declare that it implements an interface?
What does a safe type assertion 'c, ok := s.(Circle)' do when s does not hold a Circle?
13. What is the empty interface (any / interface{}) and when should you use it?

An interface with zero methods is satisfied by every type in Go. Written as interface{} or its alias any (Go 1.18+), it lets a variable or function parameter hold a value of any type. It is Go's mechanism for truly generic containers β€” at the cost of compile-time type safety.

// any is an alias for interface{} (Go 1.18+)
func describe(v any) {
    fmt.Printf("Type: %-10T  Value: %v\n", v, v)
}

describe(42)              // Type: int         Value: 42
describe("hello")         // Type: string      Value: hello
describe([]int{1, 2, 3})  // Type: []int       Value: [1 2 3]

// Heterogeneous collection
row := []any{1, "Alice", true, 3.14}

// Must type-assert to get the concrete value back
var v any = "hello"
s, ok := v.(string)  // safe β€” ok=false if not a string
// s2 := v.(int)     // panics if v is not an int

// Type switch β€” handle multiple possible types
for _, item := range row {
    switch x := item.(type) {
    case int:    fmt.Println("int:", x)
    case string: fmt.Println("string:", x)
    case bool:   fmt.Println("bool:", x)
    default:     fmt.Println("other:", x)
    }
}

// PREFER: narrow interfaces, specific types, or generics over 'any'
// 'any' loses compile-time type safety β€” errors become runtime panics
What is the 'any' type in Go 1.18+?
Why should 'any' / interface{} be used sparingly?
14. How do pointers work in Go and how are they safer than C pointers?

Go has pointers β€” variables that store the memory address of another variable β€” but removes the dangerous parts of C pointers. There is no pointer arithmetic, no manual memory management, and the garbage collector handles deallocation. A pointer to a local variable is safe to return from a function.

x := 42
p := &x          // & gives the address of x; p is *int
fmt.Println(*p)  // 42 β€” * dereferences: gives the value at the address

*p = 100         // modify x through the pointer
fmt.Println(x)   // 100

// new() β€” allocates a zeroed value and returns its pointer
q := new(int)    // *int pointing to 0
*q = 55

// nil pointer β€” the zero value for any pointer type
var ptr *int
fmt.Println(ptr)      // 
// fmt.Println(*ptr)  // PANIC: nil pointer dereference

// Passing by pointer β€” allows a function to modify the caller's variable
func increment(n *int) { *n++ }
val := 10
increment(&val)
fmt.Println(val)  // 11

// SAFE: returning pointer to local β€” Go's escape analysis handles this
type Point struct{ X, Y int }
func newPoint(x, y int) *Point {
    p := Point{x, y}  // may be allocated on heap by compiler
    return &p          // safe in Go; would be dangling pointer in C!
}

// Auto-dereference on struct pointers β€” no -> operator needed
pp := &Point{1, 2}
pp.X = 10  // same as (*pp).X = 10
What does the '&' operator produce in Go?
Is it safe to return a pointer to a locally declared variable in Go?
15. How does Go handle errors, and what is the difference between %v and %w in fmt.Errorf?

Go treats errors as values returned by functions, not as exceptions thrown from the call stack. This makes error handling explicit and visible. Every function that can fail returns an error as its last return value. The caller is responsible for handling it.

// error is a built-in interface: type error interface { Error() string }

// Standard pattern
func parseAge(s string) (int, error) {
    age, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("parseAge: %w", err)  // %w wraps the error
    }
    if age < 0 || age > 150 {
        return 0, fmt.Errorf("parseAge: invalid age %d", age)  // %v or plain
    }
    return age, nil
}

// Sentinel errors β€” compare with errors.Is()
var ErrNotFound = errors.New("not found")

// %w wraps the error β€” errors.Is / errors.As can inspect the chain
wrapped := fmt.Errorf("lookupUser: %w", ErrNotFound)
fmt.Println(errors.Is(wrapped, ErrNotFound))  // true

// %v formats as string β€” errors.Is CANNOT find original error
notWrapped := fmt.Errorf("lookupUser: %v", ErrNotFound)
fmt.Println(errors.Is(notWrapped, ErrNotFound))  // false!

// Custom error type
type ValidationError struct{ Field, Msg string }
func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}

// errors.As β€” extract a specific error type from the chain
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("bad field:", ve.Field)
}
What is the key difference between '%w' and '%v' in fmt.Errorf?
What does errors.Is(err, target) do?
16. What are goroutines and how do you use sync.WaitGroup to wait for them?

A goroutine is a lightweight, concurrently executing function managed by the Go runtime. The cost to create one is ~2 KB of stack and ~300 ns β€” roughly 1000Γ— cheaper than an OS thread. The runtime multiplexes goroutines onto OS threads with its own scheduler.

// Launch a goroutine with the 'go' keyword
go fmt.Println("running concurrently")

// Anonymous goroutine
go func(msg string) {
    fmt.Println(msg)
}("hello from goroutine")

// PROBLEM: main() may return before goroutines finish

// SOLUTION: sync.WaitGroup
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)           // register ONE more goroutine β€” do this BEFORE go
    go func(n int) {
        defer wg.Done() // signal this goroutine is complete
        fmt.Printf("worker %d\n", n)
    }(i)
}

wg.Wait()  // block until all goroutines call Done() β€” counter reaches 0
fmt.Println("all workers finished")

// Output (order may vary):
// worker 3
// worker 1
// worker 0
// worker 4
// worker 2
// all workers finished
What keyword starts a new goroutine in Go?
Why must wg.Add(1) be called BEFORE launching the goroutine?
17. What are channels in Go and what is the difference between buffered and unbuffered?

Channels are typed conduits for sending values between goroutines. They are goroutine-safe and provide the synchronisation primitive underlying Go's concurrency model. Go's philosophy: "Do not communicate by sharing memory; instead, share memory by communicating."

// Unbuffered channel β€” send BLOCKS until a receiver is ready
ch := make(chan int)
go func() { ch <- 42 }()  // goroutine parks until main receives
v := <-ch                  // unblocks sender
fmt.Println(v)             // 42

// Buffered channel β€” send blocks only when buffer is FULL
buf := make(chan string, 3)
buf <- "a"  // no goroutine needed β€” goes into buffer
buf <- "b"
fmt.Println(<-buf)  // a (FIFO)
fmt.Println(<-buf)  // b

// Closing a channel signals: no more values will be sent
jobs := make(chan int, 5)
for i := 0; i < 5; i++ { jobs <- i }
close(jobs)

// Range β€” exits automatically when channel is closed and drained
for j := range jobs { fmt.Println(j) }  // 0 1 2 3 4

// Comma-ok: detect closed channel
val, ok := <-jobs
fmt.Println(val, ok)  // 0 false

// select β€” wait on multiple channel operations
select {
case v := <-ch1:            fmt.Println("ch1:", v)
case v := <-ch2:            fmt.Println("ch2:", v)
case <-time.After(time.Second): fmt.Println("timeout")
}
What happens when you send to an unbuffered channel and no goroutine is receiving?
What does receiving from a closed, empty channel return (using the comma-ok form)?
18. How do defer, panic, and recover work together in Go?

defer schedules a function call to run when the surrounding function returns β€” regardless of how it returns (normally, via error, or via panic). It is Go's idiomatic resource-cleanup mechanism. panic stops normal execution; recover catches it inside a deferred function.

// defer β€” executes when surrounding function returns
func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil { return err }
    defer f.Close()  // ALWAYS runs, even if function returns early
    // process file...
    return nil
}

// Multiple defers: LIFO order (last-in, first-out)
func demo() {
    defer fmt.Println("3rd")  // runs first (LIFO)
    defer fmt.Println("2nd")
    defer fmt.Println("1st")  // runs last? NO β€” runs first!
    // Output order: 1st, 2nd, 3rd  β€” wait, no:
    // ACTUAL order: 3rd β†’ 2nd β†’ 1st  (LIFO!)
}

// Defer argument evaluation: immediate, not deferred!
x := 5
defer fmt.Println(x)  // prints 5 β€” x evaluated NOW
x = 10                // too late to affect the defer

// panic β€” signals an unrecoverable error
func mustPositive(n int) int {
    if n <= 0 { panic(fmt.Sprintf("expected positive, got %d", n)) }
    return n
}

// recover β€” catches a panic; ONLY useful inside defer
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    return a / b, nil  // panics if b == 0
}
In what order do multiple deferred calls execute?
For recover() to catch a panic, where must it be called?
19. What are closures in Go and what is the loop variable capture gotcha?

A closure is a function that references and closes over variables from its surrounding scope. The closure captures the variable itself β€” not a copy of its value at the moment of creation. This distinction is the source of one of the most common Go bugs.

// Closure capturing outer variable
func makeCounter() func() int {
    count := 0
    return func() int {
        count++         // captures 'count' β€” reads its current value each call
        return count
    }
}

c := makeCounter()
fmt.Println(c(), c(), c())  // 1 2 3

c2 := makeCounter()         // independent counter
fmt.Println(c2())           // 1 (fresh)

// CLASSIC GOTCHA: loop variable capture
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
    funcs[i] = func() { fmt.Println(i) }  // captures &i, not a copy!
}
funcs[0]()  // prints 3 (not 0!)
funcs[1]()  // prints 3
funcs[2]()  // prints 3
// By the time any func runs, the loop ended and i == 3

// FIX 1: pass i as an argument (creates a per-iteration copy)
for i := 0; i < 3; i++ {
    go func(n int) { fmt.Println(n) }(i)  // n is a copy of i
}

// FIX 2: shadow i with a new variable per iteration
for i := 0; i < 3; i++ {
    i := i  // new 'i' that belongs to this iteration
    funcs[i] = func() { fmt.Println(i) }
}

// Go 1.22+: loop variables are per-iteration by default β€” bug gone!
In the loop variable capture gotcha, why do all closures print the same final value?
What was changed in Go 1.22 to eliminate the loop variable capture problem?
20. What is the init() function and when does it run?

init() is an optional special function that runs automatically after all package-level variable initialisations β€” before main(). It takes no arguments, returns nothing, and cannot be called directly. A single file may contain multiple init() functions.

package main

import (
    "fmt"
    _ "github.com/lib/pq"  // blank import: run pq's init() for side effects
)                            // registers the postgres driver

var cfg *Config

func init() {
    // Runs BEFORE main(), AFTER package-level vars are initialised
    var err error
    cfg, err = loadConfig("app.yaml")
    if err != nil {
        panic(fmt.Sprintf("config init failed: %v", err))
    }
    fmt.Println("config loaded")
}

func main() {
    fmt.Println("main running")
    // cfg is guaranteed to be non-nil here
}

// INITIALISATION ORDER within a package:
// 1. Package-level variables (in dependency order)
// 2. init() functions (in source file order, multiple per file allowed)
// 3. main() β€” only in package main

// Across packages: imported packages initialise first
// Go guarantees no circular init dependencies
When exactly does the init() function run?
What does a blank import 'import _ "package"' do?
21. What is the Go module system? What do go.mod and go.sum contain?

Modules are Go's dependency management system (stable since Go 1.13). A module is a collection of related packages identified by a module path (typically a repository URL). The module's root contains a go.mod file that records the module path, the minimum Go version, and all required dependencies.

// Initialise a module
// $ go mod init github.com/alice/myapp

// go.mod β€” human-editable; commit to version control
// ─────────────────────────────────────────────────
// module github.com/alice/myapp
//
// go 1.22
//
// require (
//     github.com/gin-gonic/gin v1.9.1
//     golang.org/x/sync        v0.6.0
// )

// go.sum β€” machine-managed; commit to version control
// Contains SHA-256 hashes of each downloaded module zip
// Guarantees tamper-proof, reproducible builds

// Key commands:
// go get github.com/pkg@v1.2.3  β€” add / upgrade dependency
// go mod tidy                    β€” remove unused, add missing deps
// go mod download                β€” pre-download all deps
// go list -m all                 β€” list entire dependency graph

// Minimum Version Selection (MVS):
// Go ALWAYS picks the minimum version that satisfies all requirements.
// Nothing upgrades silently β€” builds are reproducible.

// Major version imports (breaking changes need new import path):
// import "github.com/alice/pkg/v2"  β€” v2 has breaking API changes
What does 'go mod tidy' do?
What is stored in go.sum?
22. What is a data race in Go and how do you detect one?

A data race occurs when two or more goroutines access the same memory location concurrently, at least one access is a write, and there is no synchronisation between them. Data races produce undefined, non-deterministic behaviour β€” results vary between runs and can silently corrupt data.

// DATA RACE β€” unsafe counter increment from multiple goroutines
var counter int
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++  // READ + INCREMENT + WRITE β€” not atomic!
    }()
}
wg.Wait()
fmt.Println(counter)  // less than 1000 β€” data was lost!

// DETECT: go run -race main.go  or  go test -race ./...
// Race detector output:
// ==================
// WARNING: DATA RACE
// Write at 0x... by goroutine 8: main.main.func1() :12
// Previous write at 0x... by goroutine 7: main.main.func1() :12
// ==================

// FIX 1: sync.Mutex
var mu sync.Mutex
go func() { mu.Lock(); counter++; mu.Unlock() }()

// FIX 2: atomic operation (faster for simple numeric ops)
var atomicCounter int64
go func() { atomic.AddInt64(&atomicCounter, 1) }()

// FIX 3: channel β€” one goroutine owns the counter
inc := make(chan struct{}, 100)
go func() { n := 0; for range inc { n++ } }()
inc <- struct{}{}  // safe
How do you detect data races in a Go program?
Why is 'counter++' not safe when accessed from multiple goroutines?
23. What is the fmt.Stringer interface and how does it control how a type is printed?

The fmt package defines the Stringer interface: a type that implements String() string controls how it appears when printed with fmt.Println, fmt.Printf("%v"), and related functions. Implementing error works the same way for error messages.

// fmt.Stringer interface:
// type Stringer interface { String() string }

type Direction int

const (
    North Direction = iota
    South; East; West
)

func (d Direction) String() string {
    return [...]string{"North", "South", "East", "West"}[d]
}

fmt.Println(North)          // North  (not: 0)
fmt.Printf("%v\n", East)   // East
fmt.Printf("%s\n", West)   // West

// Another example: Point with custom format
type Point struct{ X, Y float64 }

func (p Point) String() string {
    return fmt.Sprintf("(%.1f, %.1f)", p.X, p.Y)
}

p := Point{3.5, 7.2}
fmt.Println(p)          // (3.5, 7.2)
fmt.Printf("%v\n", p)  // (3.5, 7.2)

// TRAP: infinite recursion inside String()
// func (p Point) String() string {
//     return fmt.Sprintf("%v", p)  // calls String() again β†’ stack overflow!
// }
// FIX: cast to a non-Stringer type
// return fmt.Sprintf("%v", struct{ X, Y float64 }(p))
Which interface must a type implement for fmt.Println to use a custom string representation?
What causes infinite recursion when implementing the Stringer interface?
24. What is the difference between a type definition and a type alias in Go?

Go has two ways to give a new name to a type, with importantly different semantics. Understanding this prevents subtle type-safety bugs and confusing compiler errors.

Type Definition vs Type Alias
AspectType Definition: type T UType Alias: type T = U
Creates new type?YES β€” T and U are distinct typesNO β€” T is just another name for U
Methods of U inherited?No β€” T starts with no methodsYes β€” T and U are identical
T β†’ U assignmentRequires explicit conversion: U(t)No conversion needed
Use forAdding type safety, defining method setsCode migration, readability aliases
// TYPE DEFINITION β€” new type with type safety
type Celsius    float64
type Fahrenheit float64

func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

var bodyTemp Celsius = 37.0
// var t Fahrenheit = bodyTemp  // COMPILE ERROR β€” different types!
var t Fahrenheit = bodyTemp.ToFahrenheit()  // OK

// Prevents bugs: you cannot accidentally mix temperatures
func setOven(temp Celsius)        { /* ... */ }
// setOven(Fahrenheit(350))       // COMPILE ERROR β€” type safety!
setOven(Celsius(180))             // OK

// TYPE ALIAS β€” same type, different name
type MyString = string

var s1 string   = "hello"
var s2 MyString = s1    // OK β€” same type
s1 = s2                 // OK β€” no conversion

// Built-in aliases you already use:
// byte = uint8
// rune = int32
// any  = interface{}
What is the key difference between 'type Celsius float64' and 'type Celsius = float64'?
Can you directly assign a Celsius value to a Fahrenheit variable if both are defined as 'type X float64'?
25. How does Go handle strings, runes, and bytes? Why is len(s) not the character count?

Go strings are immutable sequences of bytes stored in UTF-8 encoding. Since UTF-8 is a variable-width encoding, a single character (rune) can occupy 1 to 4 bytes. This means len(s) reports bytes, not characters β€” a fact that trips up many beginners.

s := "Hello, δΈ–η•Œ"  // UTF-8 string containing ASCII + Chinese chars

// len() counts BYTES
fmt.Println(len(s))                       // 13
// 7 ASCII (1 byte each) + 2 Chinese (3 bytes each) = 13

// Count RUNES (Unicode characters)
fmt.Println(utf8.RuneCountInString(s))    // 9
fmt.Println(len([]rune(s)))               // 9 (same)

// Byte-by-byte iteration β€” WRONG for multi-byte chars
for i := 0; i < len(s); i++ {
    fmt.Printf("%d:%x ", i, s[i])  // raw byte values
}

// Rune-by-rune iteration β€” CORRECT for Unicode
// range decodes UTF-8 runes automatically
for i, r := range s {  // i = byte offset, r = rune (Unicode code point)
    fmt.Printf("%d:%c ", i, r)
}

// Conversions
b := []byte(s)      // string β†’ []byte (copies)
r := []rune(s)      // string β†’ []rune (copies)
s2 := string(b)     // []byte β†’ string
s3 := string(r)     // []rune β†’ string

// Efficient string building (avoid + in loops)
var sb strings.Builder
for _, word := range []string{"Go", " ", "rocks"} {
    sb.WriteString(word)
}
fmt.Println(sb.String())  // Go rocks
Why does len("Hello, δΈ–η•Œ") return 13 instead of 9?
Which for-loop construct correctly iterates over the Unicode characters of a Go string?
26. What is a goroutine leak and what is the idiomatic way to prevent one?

A goroutine leak occurs when a goroutine is started but never terminates β€” it stays blocked forever waiting on a channel, mutex, or network call that will never complete. Goroutines are cheap but not free: leaked goroutines accumulate over time and eventually exhaust memory in long-running services.

// LEAK β€” goroutine blocks forever; nobody ever sends to ch
func leaky() {
    ch := make(chan int)
    go func() {
        v := <-ch         // blocks indefinitely
        fmt.Println(v)
    }()
    // function returns β€” goroutine is stuck forever!
}

// FIX: pass a context and select on ctx.Done()
func safe(ctx context.Context, ch <-chan int) {
    go func() {
        select {
        case v := <-ch:
            fmt.Println(v)
        case <-ctx.Done():  // goroutine exits cleanly when cancelled
            return
        }
    }()
}

// FIX: close the channel to unblock receivers
func producer() <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)   // close signals: no more values
        for _, v := range data {
            ch <- v
        }
    }()
    return ch
}

// Detect leaks in tests:
// defer goleak.VerifyNone(t)  β€” fails test if goroutines remain
// runtime.NumGoroutine()      β€” watch for steady growth
What is a goroutine leak?
What is the most idiomatic Go mechanism for giving a goroutine a way to exit cleanly?
27. What is sync.Mutex and when do you use sync.RWMutex instead?

sync.Mutex provides mutual exclusion β€” at most one goroutine holds the lock at any moment. sync.RWMutex is an extension: multiple goroutines can hold a read lock simultaneously, but a write lock is exclusive. Use RWMutex when reads vastly outnumber writes.

// sync.Mutex β€” protect any shared mutable state
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Add(n int) {
    c.mu.Lock()
    defer c.mu.Unlock()  // always use defer β€” runs even on panic
    c.count += n
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// sync.RWMutex β€” read-heavy workloads (e.g. config, caches)
type SafeConfig struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *SafeConfig) Get(key string) string {
    c.mu.RLock()           // multiple goroutines can hold RLock at once
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *SafeConfig) Set(key, val string) {
    c.mu.Lock()            // exclusive β€” no readers OR writers allowed
    defer c.mu.Unlock()
    c.data[key] = val
}

// RULES:
// 1. Never copy a Mutex after first use (lock state would be duplicated)
// 2. Always pass struct containing Mutex as a pointer (*SafeCounter)
// 3. Mutex is NOT reentrant β€” a goroutine holding Lock() will deadlock
//    if it calls Lock() again on the same mutex
What is the key advantage of sync.RWMutex over sync.Mutex?
Why must structs containing a Mutex always be passed as pointers?
28. How does struct embedding promote methods in Go, and how does it differ from inheritance?

Go uses composition through embedding rather than class inheritance. When a type is embedded (without a field name), its exported methods and fields are promoted to the outer type β€” they are directly accessible. However, the outer type is NOT a subtype of the embedded type.

type Logger struct{ prefix string }

func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.prefix, msg)
}

// Server embeds Logger β€” gains the Log method
type Server struct {
    *Logger               // embedded pointer (promotes Log)
    host string
    port int
}

srv := Server{
    Logger: &Logger{prefix: "SERVER"},
    host: "localhost",
    port: 8080,
}
srv.Log("starting")        // SERVER: starting β€” promoted method!
srv.Logger.Log("starting") // same thing, explicit call

// Method override β€” Server can define its own Log
func (s *Server) Log(msg string) {
    s.Logger.Log(fmt.Sprintf("%s:%d β€” %s", s.host, s.port, msg))
}

// KEY: embedding is NOT inheritance
type Describer interface{ Describe() string }
type Base struct{}
func (Base) Describe() string { return "I am Base" }

type Derived struct{ Base }
// Derived satisfies Describer via promotion:
var d Describer = Derived{}  // works!

// BUT: you CANNOT use Derived where Base is required
func process(b Base) {}
// process(Derived{})  // COMPILE ERROR β€” not a subtype!
What is 'method promotion' through embedding?
Can a struct with an embedded type be passed where the embedded type is expected as a function parameter?
29. How does append() work internally in Go? When does it allocate new memory?

append adds elements to a slice. When there is spare capacity in the backing array, it writes directly into it and increments the length β€” no allocation. When capacity is exhausted, it allocates a new, larger array, copies all elements, and returns a new slice header. This is why you must always assign the return value of append.

s := make([]int, 3, 5)     // len=3, cap=5, backing array has 2 spare slots
fmt.Println(len(s), cap(s)) // 3 5

s = append(s, 10)           // len=4, cap=5 β€” no allocation (has room)
s = append(s, 20)           // len=5, cap=5 β€” no allocation (fills last slot)
s = append(s, 30)           // len=6, cap>=10 β€” REALLOCATES! new backing array
fmt.Println(len(s), cap(s)) // 6  10  (capacity grew to ~2x)

// Sub-slice shares backing array β€” subtle bug territory
a := []int{1, 2, 3, 4, 5}
b := a[1:3]                 // [2 3] β€” shares backing array with a
fmt.Println(cap(b))          // 4 (from position 1 to end of a's array)

b = append(b, 99)           // writes to a[3]! No reallocation needed
fmt.Println(a)               // [1 2 3 99 5] β€” a was modified!

// FIX: use copy to get an independent slice
b2 := make([]int, len(a[1:3]))
copy(b2, a[1:3])             // independent backing array
b2 = append(b2, 99)         // safe β€” doesn't affect a

// Pre-allocate when final length is known β€” avoids repeated reallocation
result := make([]int, 0, len(input))  // cap=len(input), no mid-loop alloc
for _, v := range input {
    result = append(result, v*2)
}
What does append() do when the slice's length equals its capacity?
Why does appending to a sub-slice sometimes modify the original slice?
30. What is context.Context and why is it the first parameter in so many Go functions?

context.Context is Go's standard mechanism for propagating three things across API boundaries and goroutine calls: cancellation signals, deadlines, and request-scoped values. Passing it as the first parameter is a Go convention β€” it allows any blocking call to be cancelled.

// The context.Context interface:
// Done()     <-chan struct{}  β€” closed when cancelled or deadline passed
// Err()      error           β€” nil, Canceled, or DeadlineExceeded
// Deadline() (time.Time, bool)
// Value(key) any

// Root contexts
ctx := context.Background()  // top-level: no deadline, no cancel
ctx  = context.TODO()        // placeholder when ctx not yet determined

// Derived contexts β€” ALWAYS defer cancel()
ctx1, cancel1 := context.WithCancel(context.Background())
defer cancel1()  // prevents goroutine leak inside context machinery

ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel2()

// Use in a worker goroutine
func fetchData(ctx context.Context, url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return nil, err }  // err contains context.DeadlineExceeded
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

// Checking cancellation in a loop
func processItems(ctx context.Context, items []Item) error {
    for _, item := range items {
        select {
        case <-ctx.Done():   // cancelled β€” stop early
            return ctx.Err()
        default:            // continue
        }
        process(item)
    }
    return nil
}
What does ctx.Done() return?
Why must you always defer the cancel function returned by context.WithCancel?
31. What is the 'typed nil' trap in Go and why does 'if err != nil' sometimes fail?

An interface value in Go has two components: a dynamic type and a dynamic value. An interface is nil only when BOTH are nil. A common mistake is returning a typed nil pointer as an error β€” the interface has a type component set, so it is NOT nil even though the pointer value is nil.

// The trap: returning a *MyError that is nil as an error interface
type MyError struct{ Code int }
func (e *MyError) Error() string { return fmt.Sprintf("error %d", e.Code) }

func riskyOperation(succeed bool) error {
    var err *MyError   // err is a nil *MyError pointer
    if !succeed {
        err = &MyError{Code: 404}
    }
    return err         // BUG: returns interface{type=*MyError, value=nil}
                       // This interface is NOT nil!
}

result := riskyOperation(true)  // supposed to succeed
if result != nil {              // UNEXPECTED: true! Interface is non-nil.
    fmt.Println("error:", result)  // prints: error 0
}

// FIX: return bare nil, not a typed nil pointer
func safeOperation(succeed bool) error {
    if !succeed {
        return &MyError{Code: 404}
    }
    return nil   // correct: returns a nil interface, not a *MyError nil
}

// Verify the fix
result2 := safeOperation(true)
fmt.Println(result2 == nil)  // true β€” correct!

// Rule: functions returning an error interface should NEVER return
// a concrete typed pointer that might be nil. Always return bare nil.
Why does a nil *MyError returned as an error interface compare as non-nil?
What is the correct way to return 'no error' from a function with an error return type?
32. What are the most important formatting verbs in Go's fmt package?

Knowing fmt format verbs lets you produce clear output for debugging, logging, and user messages. The %v verb is the universal default; specialised verbs give more control.

Key fmt Format Verbs
VerbMeaningExample output
%vDefault format for any value42, true, [1 2 3]
%+vStruct with field names{Name:Alice Age:30}
%#vGo-syntax representationmain.Person{Name:"Alice", Age:30}
%TType of the valueint, []string, main.Person
%dInteger in decimal42
%f / %.2fFloat / float with 2 decimal places3.141590 / 3.14
%sPlain stringhello
%qQuoted string"hello"
%xHex encodingff (int) or 68656c6c6f (string)
%pPointer address0xc000012080
%wWrap an error (Errorf only)used for error chaining
name := "Alice"
age  := 30
pi   := 3.14159

fmt.Printf("%s is %d years old\n", name, age)  // Alice is 30 years old
fmt.Printf("Pi β‰ˆ %.2f\n", pi)                  // Pi β‰ˆ 3.14
fmt.Printf("Type: %T\n", pi)                   // Type: float64

// Sprintf β€” format to string, no output
msg := fmt.Sprintf("User: %s (age %d)", name, age)

// Errorf β€” format and create an error
err := fmt.Errorf("user %q not found (id: %d)", name, 99)

// Width and padding
fmt.Printf("%10s\n", "right")   //      right  (right-align, width 10)
fmt.Printf("%-10s|\n", "left")   // left      |  (left-align, width 10)
fmt.Printf("%06d\n", 42)         // 000042      (zero-pad to width 6)

// Printing structs
type Person struct{ Name string; Age int }
p := Person{"Bob", 25}
fmt.Printf("%v\n",  p)  // {Bob 25}
fmt.Printf("%+v\n", p)  // {Name:Bob Age:25}
fmt.Printf("%#v\n", p)  // main.Person{Name:"Bob", Age:25}
Which fmt verb prints a struct with its field names included?
What does fmt.Sprintf do?
«
»
Golang Internals and Memory Management Interview Questions

Comments & Discussions