Skip to content

PUBLIC QUESTION PREVIEW

Go / Golang interview questions

Goroutines, channels, interfaces, error handling, HTTP server, context, generics, and idiomatic Go patterns.

Showing 12 of 50 source questions. Sample answers are study material, not employer-specific hiring guidance.

How to work through this preview

Attempt each question before reading its answer. For a concept, give a definition and a concrete example. For code, state constraints, walk through a small input, and check complexity. For design, separate requirements from assumptions and explain failure handling. Use real situations for behavioral answers.

QUESTION 1 / easy / conceptual

What is Go? What are its key design goals?

Sample answer

Go (Golang): a statically typed, compiled programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson, released in 2009. Design goals: - Simplicity: small language spec (~25 keywords). Readable code looks similar regardless of who wrote it. - Fast compilation: compiles entire programs in seconds even for large codebases - Built-in concurrency: goroutines and channels are first-class language features, not library add-ons - Safety: garbage collected (no manual memory management), strong typing - Practical: pragmatic tooling (go fmt, go vet, go test built in) Key characteristics: - Compiled to native machine code (fast execution, single binary) - Garbage collected (no malloc/free) - No inheritance, no generics until 1.18 — favours composition - No exceptions — errors are values (explicit error handling) - Interfaces are structural (duck typing) — no implements keyword needed - Goroutines: lightweight threads multiplexed onto OS threads by the Go runtime What Go is used for: - Cloud infrastructure (Kubernetes, Docker, Terraform — all written in Go) - Microservices and HTTP APIs - CLI tools (Hugo, Cobra) - Networking tools, proxies - Indian fintech backend (Razorpay, PhonePe use Go internally) Go is NOT a replacement for Python (data science), Java (enterprise Spring), or C (systems programming). It fills the middle ground: performance of compiled languages with development speed of scripted languages.

Hints to guide your next attempt

  • Compiled, statically typed, garbage collected
  • Simplicity, concurrency, fast compile times

Source tags: golang / introduction / design-goals / concurrency

QUESTION 2 / easy / conceptual

How does Go handle errors? What is the error interface?

Sample answer

Go has no exceptions. Errors are plain values implementing the error interface. The error interface: type error interface { Error() string } Any type with an Error() string method implements error. Convention: functions that can fail return (result, error) as the last return value. func readFile(path string) ([]byte, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } return data, nil } Calling code must check err explicitly: data, err := readFile("config.json") if err != nil { log.Fatal(err) } // data is now safe to use Creating errors: // Simple error return fmt.Errorf("user %d not found", userID) // Custom error type type NotFoundError struct { Resource string ID int } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s with ID %d not found", e.Resource, e.ID) } return &NotFoundError{Resource: "user", ID: 42} Wrapping errors (Go 1.13+): return fmt.Errorf("createOrder: %w", dbErr) // %w wraps, %v doesn't Unwrapping: var notFound *NotFoundError if errors.As(err, &notFound) { // err is or wraps a *NotFoundError } if errors.Is(err, sql.ErrNoRows) { // err is or wraps sql.ErrNoRows } Philosophy: explicit error handling is verbose but makes error paths obvious in code. There are no hidden control-flow jumps like throw/catch.

Hints to guide your next attempt

  • Errors are values returned as the last return value
  • if err != nil pattern

Source tags: error-handling / golang / error-interface / errors-as / errors-is

QUESTION 3 / easy / conceptual

What are goroutines? How do they differ from OS threads?

Sample answer

Goroutine: a lightweight, concurrent unit of execution managed by the Go runtime. Started with the go keyword. go doWork() // launches doWork() as a goroutine go func() { // anonymous goroutine fmt.Println("hello") }() Goroutines vs OS threads: | Aspect | OS Thread | Goroutine | | Memory | ~1-8 MB stack | ~2 KB initial stack (grows dynamically) | | Startup | Expensive (kernel call) | Very cheap (microseconds) | | Scheduling | OS scheduler | Go runtime scheduler (M:N) | | Switching | Expensive (kernel mode) | Cheap (user space) | | Typical count | Hundreds | Millions | M:N scheduling model: - The Go runtime maps M goroutines onto N OS threads (M >> N) - GOMAXPROCS: number of OS threads used for goroutine execution (default = CPU cores) - When a goroutine blocks on I/O, the runtime parks it and runs another goroutine on the same thread Why goroutines are practical: - Start thousands for concurrent HTTP handlers, background jobs, pipeline stages - Starting an OS thread per connection (Java/Python threading) doesn't scale beyond thousands Lifecycle: func main() { go func() { time.Sleep(100 * time.Millisecond) fmt.Println("goroutine done") }() // main goroutine must wait — if main() returns, all goroutines are killed time.Sleep(200 * time.Millisecond) } In practice: use sync.WaitGroup to wait for goroutines to finish, or channels to communicate results.

Hints to guide your next attempt

  • Lightweight, multiplexed onto OS threads by the Go runtime
  • go keyword starts a goroutine

Source tags: goroutines / concurrency / golang / threads / scheduling

QUESTION 4 / medium / conceptual

What are channels in Go? Explain buffered vs unbuffered channels.

Sample answer

Channel: a typed conduit through which goroutines send and receive values. "Don't communicate by sharing memory; share memory by communicating." Creating channels: ch := make(chan int) // unbuffered ch := make(chan string, 10) // buffered, capacity 10 Send and receive: ch <- 42 // send (blocks until receiver ready, for unbuffered) val := <-ch // receive (blocks until sender sends) val, ok := <-ch // ok is false if channel is closed and empty Unbuffered channel (synchronous rendezvous): - Send blocks until a receiver is ready; receive blocks until a sender sends - Provides synchronisation guarantees: after send returns, the receiver got the value ch := make(chan int) go func() { ch <- 99 }() // goroutine blocks at send until main receives fmt.Println(<-ch) // 99 Buffered channel: - Send blocks only if buffer is FULL; receive blocks only if buffer is EMPTY - Decouples sender and receiver timing ch := make(chan int, 3) ch <- 1 // doesn't block (buffer has space) ch <- 2 ch <- 3 ch <- 4 // blocks (buffer full) Closing channels: close(ch) // signals no more values will be sent // Receivers can detect closure: for val := range ch { fmt.Println(val) } // exits loop when ch is closed Direction types (restrict channel usage): func producer(ch chan<- int) { ch <- 42 } // send-only func consumer(ch <-chan int) { <-ch } // receive-only Real use pattern — pipeline: func gen(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out }

Hints to guide your next attempt

  • Channels communicate between goroutines
  • Unbuffered: synchronous; Buffered: async up to capacity

Source tags: channels / goroutines / concurrency / buffered / unbuffered / golang

QUESTION 5 / medium / conceptual

What is the select statement in Go? When do you use it?

Sample answer

select: Go's way to wait on multiple channel operations simultaneously. It picks whichever case is ready; if multiple are ready, it picks one at random. Syntax: select { case val := <-ch1: fmt.Println("received from ch1:", val) case ch2 <- "hello": fmt.Println("sent to ch2") case <-time.After(2 * time.Second): fmt.Println("timeout!") default: fmt.Println("no channel ready") } Common patterns: 1. Timeout: select { case result := <-resultCh: return result, nil case <-time.After(3 * time.Second): return nil, fmt.Errorf("operation timed out") } 2. Cancellation with context: select { case msg := <-dataCh: process(msg) case <-ctx.Done(): return ctx.Err() // context cancelled or deadline exceeded } 3. Non-blocking channel check (with default): select { case msg := <-ch: fmt.Println("got:", msg) default: fmt.Println("no message ready") } 4. Fan-in (merge multiple channels): func merge(cs ...<-chan int) <-chan int { out := make(chan int) for _, c := range cs { go func(c <-chan int) { for v := range c { out <- v } }(c) } return out } 5. Graceful shutdown: for { select { case job := <-jobCh: process(job) case <-quit: return } }

Hints to guide your next attempt

  • Like a switch for channels
  • Wait on multiple channel operations simultaneously

Source tags: select / channels / concurrency / timeout / golang

QUESTION 6 / medium / conceptual

What is the sync package in Go? Explain Mutex and WaitGroup.

Sample answer

sync package: provides synchronisation primitives for coordinating goroutines accessing shared state. sync.Mutex — mutual exclusion lock: Protects shared state from concurrent reads and writes (race condition prevention). type SafeCounter struct { mu sync.Mutex count int } func (c *SafeCounter) Increment() { c.mu.Lock() defer c.mu.Unlock() // always unlock even if function panics c.count++ } func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.count } sync.RWMutex — read/write mutex: - Multiple goroutines can hold read lock simultaneously - Write lock is exclusive var mu sync.RWMutex var cache map[string]string func read(key string) string { mu.RLock() defer mu.RUnlock() return cache[key] } func write(key, val string) { mu.Lock() defer mu.Unlock() cache[key] = val } sync.WaitGroup — wait for N goroutines to complete: var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) // must Add before starting the goroutine go func(n int) { defer wg.Done() // called when goroutine finishes process(n) }(i) } wg.Wait() // blocks until all 5 goroutines call Done() sync.Once — run code exactly once across concurrent goroutines: var once sync.Once once.Do(func() { // init database connection db = connectDB() }) sync.Map — concurrent-safe map: var m sync.Map m.Store("key", "value") v, ok := m.Load("key") m.Range(func(k, v interface{}) bool { fmt.Println(k, v); return true })

Hints to guide your next attempt

  • Mutex for shared state protection, WaitGroup to wait for goroutines

Source tags: sync / mutex / waitgroup / concurrency / golang

QUESTION 7 / easy / conceptual

How do interfaces work in Go? What is implicit implementation?

Sample answer

Interface: a set of method signatures. A type satisfies an interface by implementing all its methods — no explicit declaration needed. Defining an interface: type Animal interface { Sound() string Name() string } Implementing implicitly — no "implements Animal" keyword: type Dog struct{ name string } func (d Dog) Sound() string { return "Woof" } func (d Dog) Name() string { return d.name } type Cat struct{ name string } func (c Cat) Sound() string { return "Meow" } func (c Cat) Name() string { return c.name } // Both Dog and Cat satisfy Animal — the compiler checks automatically func makeNoise(a Animal) string { return a.Name() + " says " + a.Sound() } makeNoise(Dog{name: "Bruno"}) // "Bruno says Woof" makeNoise(Cat{name: "Mimi"}) // "Mimi says Meow" Common interfaces from the standard library: - fmt.Stringer: String() string - io.Reader: Read(p []byte) (n int, err error) - io.Writer: Write(p []byte) (n int, err error) - error: Error() string - sort.Interface: Len(), Less(i,j), Swap(i,j) Empty interface (interface{}): satisfied by every type. Go 1.18+: use any as an alias for interface{}. func PrintAny(v interface{}) { fmt.Println(v) } Type assertion and type switch: // Type assertion: s, ok := v.(string) // safely check if v is a string // Type switch: switch val := v.(type) { case int: fmt.Println("int:", val) case string: fmt.Println("string:", val) default: fmt.Printf("unknown: %T ", val) } Interface value internals: an interface value holds a (type, value) pair. A nil interface has both as nil; a non-nil interface with a nil pointer has type set but value nil — a common source of subtle bugs.

Hints to guide your next attempt

  • No implements keyword
  • If a type has the right methods, it satisfies the interface

Source tags: interfaces / golang / duck-typing / type-assertion / implicit

QUESTION 8 / easy / conceptual

What are Go structs? How do you embed structs?

Sample answer

Struct: Go's way to group related fields. No classes in Go — structs + methods + interfaces compose behaviour. Basic struct: type User struct { ID int Name string Email string } // Create: u := User{ID: 1, Name: "Rahul", Email: "rahul@test.com"} // or: var u User u.Name = "Rahul" Methods on structs: // Pointer receiver (can modify struct, preferred for mutation): func (u *User) UpdateEmail(email string) { u.Email = email } // Value receiver (read-only, gets a copy): func (u User) String() string { return u.Name + " <" + u.Email + ">" } Struct embedding (Go's "inheritance"): Go has NO inheritance, but struct embedding promotes the embedded type's fields and methods. type Base struct { ID int CreatedAt time.Time } func (b *Base) SetID(id int) { b.ID = id } type User struct { Base // embedded (anonymous field) Name string Email string } u := User{} u.ID = 1 // promoted — access Base.ID directly u.SetID(1) // promoted — access Base.SetID directly u.Base.ID = 1 // explicit access always works too Embedding multiple types: type ReadWriter struct { io.Reader io.Writer } Struct tags (used by JSON, database mappers): type User struct { ID int `json:"id" db:"id"` Name string `json:"name" db:"name"` Email string `json:"email" db:"email"` } Anonymous structs (one-off use): config := struct { Host string Port int }{Host: "localhost", Port: 8080}

Hints to guide your next attempt

  • No classes or inheritance
  • Embedding promotes fields and methods

Source tags: structs / embedding / methods / golang / composition

QUESTION 9 / easy / conceptual

What are slices in Go? How do they differ from arrays?

Sample answer

Array: fixed-size sequence. Type includes size: [3]int != [4]int. Rarely used directly. arr := [3]int{1, 2, 3} Slice: a dynamic view into an underlying array. Has three components: - Pointer to underlying array - Length (len): current number of elements - Capacity (cap): max elements before reallocation s := []int{1, 2, 3} // slice literal s := make([]int, 5) // len=5, cap=5, all zeros s := make([]int, 3, 10) // len=3, cap=10 Append: s = append(s, 4, 5) // add elements s = append(s1, s2...) // append another slice // When len == cap: Go allocates a new larger array (doubles, roughly), copies data // This is why append must always assign back: s = append(s, ...) Slicing (creates a new slice header, shares underlying array): s := []int{0, 1, 2, 3, 4} sub := s[1:4] // [1, 2, 3] — elements 1,2,3 (4 is exclusive) sub[0] = 99 // modifies s[1] too! (shared underlying array) copy to avoid shared backing array: dst := make([]int, len(s)) copy(dst, s) Nil slice vs empty slice: var s []int // nil slice — len=0, cap=0, s == nil s := []int{} // empty slice — len=0, cap=0, s != nil s := make([]int, 0) Both are safe to range over and append to. json.Marshal(nil) → "null" vs json.Marshal([]int{}) → "[]". Common patterns: // Filter in place: n := 0 for _, v := range s { if keep(v) { s[n] = v; n++ } } s = s[:n] // Remove element at index i: s = append(s[:i], s[i+1:]...)

Hints to guide your next attempt

  • Slice is a view into an array: len, cap, underlying array
  • Slices grow dynamically, arrays have fixed length

Source tags: slices / arrays / golang / append / make

QUESTION 10 / easy / conceptual

What are Go maps? What are their key properties?

Sample answer

Map: Go's built-in hash map (associative array). Keys must be comparable types; values can be any type. Creating maps: m := map[string]int{"apple": 1, "banana": 2} // literal m := make(map[string]int) // empty map var m map[string]int // nil map — can read but PANICS on write Operations: m["key"] = 42 // set val := m["key"] // get (returns zero value if key missing, not an error) val, ok := m["key"] // ok pattern: ok=false if key doesn't exist delete(m, "key") // delete len(m) // number of entries Always use the ok pattern when key may be absent: val, ok := m["missing"] if !ok { return fmt.Errorf("key not found") } Iterating (order is random by design — Go maps are unordered): for k, v := range m { fmt.Println(k, v) } Maps are NOT safe for concurrent use: // Race condition: go func() { m["x"] = 1 }() go func() { _ = m["x"] }() // Fixes: // 1. sync.RWMutex to protect reads and writes // 2. sync.Map for concurrent access Common patterns: // Set as a map of struct{}: visited := map[string]struct{}{} visited["node-1"] = struct{}{} if _, ok := visited["node-1"]; ok { ... } // Counting: freq := map[string]int{} for _, word := range words { freq[word]++ } // Grouping: byRole := map[string][]User{} byRole[u.Role] = append(byRole[u.Role], u)

Hints to guide your next attempt

  • Hash maps, unordered, not safe for concurrent use without sync
  • Check ok when reading

Source tags: maps / golang / hash-map / concurrency / range

QUESTION 11 / medium / conceptual

What is defer in Go? Explain its execution order and common uses.

Sample answer

defer: schedules a function call to run just before the surrounding function returns, regardless of how it returns (normal, return value, panic). Basic usage: func readFile(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() // will run when readFile returns, even on error // work with f... return nil } Execution order — LIFO (Last In, First Out): defer fmt.Println("3rd") defer fmt.Println("2nd") defer fmt.Println("1st") // Output (when function exits): 1st, 2nd, 3rd Arguments evaluated at defer time, not at execution time: x := 10 defer fmt.Println(x) // captures x=10 NOW x = 99 // Output: 10 (not 99) Named return values + defer — can modify return value: func double(n int) (result int) { defer func() { result *= 2 }() result = n return // deferred function runs, doubles result } double(3) → 6 Common uses: 1. Resource cleanup (file, DB connection, lock): defer f.Close() defer db.Close() defer mu.Unlock() 2. Recover from panics: func safeRun() { defer func() { if r := recover(); r != nil { fmt.Println("recovered:", r) } }() panic("something went wrong") } 3. Timing: start := time.Now() defer func() { fmt.Printf("took %v ", time.Since(start)) }() Defer overhead: minimal but real. In hot loops, avoid defer — call cleanup directly.

Hints to guide your next attempt

  • Runs just before function returns
  • LIFO order, arguments evaluated at defer time

Source tags: defer / golang / cleanup / recover / lifo

QUESTION 12 / medium / conceptual

What is panic and recover in Go?

Sample answer

panic: stops normal execution, unwinds the stack running deferred functions, and terminates the program if not recovered. panic("something terrible happened") panic(fmt.Errorf("user %d not found", id)) When to use panic: - Programming errors that should never happen in correct code (impossible states) - Array out of bounds, nil pointer dereference (Go runtime panics automatically) - Initialisation failures that make the program useless (missing required config) - NOT for normal error handling — use error returns instead recover: captures a panic inside a deferred function and prevents program termination. func safeDivide(a, b int) (result int, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered panic: %v", r) } }() return a / b, nil // panics if b == 0 } result, err := safeDivide(10, 0) // returns 0, "recovered panic: runtime error: ..." Important rules: - recover() only works inside a deferred function called directly from the panicking goroutine - recover() outside of defer returns nil - Each goroutine must have its own panic handler — a panic in goroutine A cannot be recovered in goroutine B HTTP server pattern (recover to avoid crashing the whole server): func middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { log.Printf("panic: %v", rec) http.Error(w, "Internal Server Error", 500) } }() next.ServeHTTP(w, r) }) } Philosophy: use errors for expected failure cases; use panic only for truly exceptional/unrecoverable situations.

Hints to guide your next attempt

  • panic = unrecoverable error, recover = catch a panic in a deferred function

Source tags: panic / recover / golang / error-handling / defer

Check understanding, not recognition

  • Can you explain the answer without reading it?
  • Can you show when the approach fails or needs a different assumption?
  • Can you connect the concept to a real project, task, or decision you have made?
  • Can you distinguish a simplified interview example from a production requirement?

A LITTLE MORE CLARITY

Frequently asked questions

How many Go / Golang questions are shown here?

This public preview shows 12 questions with the supplied answers from the app's 50-question Go / Golang bank. Full interactive practice requires signing in.

Should I memorize the sample answers?

Use them as reference explanations, not a script or a guarantee of technical correctness for every environment. Explain your assumptions, validate code against your language and version, and discuss trade-offs. Behavioral answers should be built from your own experience.

What do hints and solution steps mean?

Hints identify ideas to consider before reading the answer. When the source question includes steps, they are shown as a separate walkthrough. Example capacities and design targets in a solution are assumptions for that exercise, not real system statistics.

BUILD YOUR NEXT OPPORTUNITY

Practice Go / Golang actively.

Sign in to use the full topic bank and interactive practice workspace.

Open Topic Practice