Complete Go Interview Preparation - Detailed Topic Guide
Week 1: Foundation & Syntax Mastery
Day 1-2: Go Basics
Variables and Constants
Go uses static typing with type inference. Variables can be declared in multiple ways:
// Explicit type declaration
var name string = "John"
var age int = 30
// Type inference
var name = "John"
var age = 30
// Short declaration (inside functions only)
name := "John"
age := 30
// Multiple variable declaration
var (
name string = "John"
age int = 30
)
Constants are immutable values known at compile time:
const Pi = 3.14159
const (
StatusOK = 200
StatusNotFound = 404
)
// iota for auto-incrementing constants
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
)
Key Interview Points:
- Zero values:
0
for numbers,""
for strings,false
for bools,nil
for pointers/maps/slices/channels - Constants are evaluated at compile time
iota
resets to 0 in each const block
Functions
Functions are first-class citizens in Go:
// Basic function
func add(a, b int) int {
return a + b
}
// Multiple return values
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Named return values
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // naked return
}
// Variadic functions
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
// Function as type
type Calculator func(int, int) int
func operate(calc Calculator, a, b int) int {
return calc(a, b)
}
Key Interview Points:
- Functions can return multiple values (common pattern: value, error)
- Named returns can improve readability but use sparingly
- Variadic functions use
...
syntax - Functions are types and can be passed as parameters
Control Structures
If statements:
// Basic if
if x > 0 {
fmt.Println("positive")
}
// If with initialization
if err := doSomething(); err != nil {
return err
}
// If-else chain
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
} else {
fmt.Println("zero")
}
For loops (only loop in Go):
// Traditional for loop
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// While-style loop
for condition {
// loop body
}
// Infinite loop
for {
// loop body
if shouldBreak {
break
}
}
// Range loop
for index, value := range slice {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
Switch statements:
// Basic switch
switch day {
case "Monday":
fmt.Println("Start of work week")
case "Friday":
fmt.Println("TGIF")
default:
fmt.Println("Regular day")
}
// Switch with expression
switch {
case x < 0:
fmt.Println("negative")
case x == 0:
fmt.Println("zero")
default:
fmt.Println("positive")
}
// Type switch
switch v := interface{}(x).(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
Day 3-4: Data Structures & Types
Arrays vs Slices
Arrays are fixed-size, value types:
// Array declaration
var arr [5]int // [0 0 0 0 0]
arr2 := [5]int{1, 2, 3, 4, 5} // [1 2 3 4 5]
arr3 := [...]int{1, 2, 3} // [1 2 3] - compiler counts
// Arrays are value types
func modifyArray(arr [3]int) {
arr[0] = 100 // This won't affect the original
}
Slices are dynamic, reference types:
// Slice creation
var slice []int // nil slice
slice = make([]int, 5) // [0 0 0 0 0]
slice = make([]int, 5, 10) // length 5, capacity 10
slice = []int{1, 2, 3, 4, 5} // slice literal
// Slice operations
slice = append(slice, 6) // Add elements
subSlice := slice[1:4] // [2 3 4] - slicing
copy(dest, src) // Copy elements
// Slice internals
fmt.Printf("len=%d cap=%d\n", len(slice), cap(slice))
Key Interview Points:
- Arrays are value types, slices are reference types
- Slice header contains pointer, length, and capacity
append()
may reallocate if capacity exceeded- Slicing creates new slice header but shares underlying array
Maps
Hash tables in Go:
// Map creation
var m map[string]int // nil map
m = make(map[string]int) // empty map
m = map[string]int{ // map literal
"apple": 5,
"banana": 3,
}
// Map operations
m["orange"] = 7 // Set value
value := m["apple"] // Get value
value, ok := m["grape"] // Check existence
delete(m, "banana") // Delete key
// Iterate over map
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
Key Interview Points:
- Maps are reference types
- Zero value is
nil
- Keys must be comparable types
- Iteration order is not guaranteed
- Use comma ok idiom to check existence
Structs and Methods
Structs are composite types:
// Struct definition
type Person struct {
Name string
Age int
Email string
}
// Struct creation
p1 := Person{Name: "John", Age: 30, Email: "john@example.com"}
p2 := Person{"Jane", 25, "jane@example.com"} // positional
var p3 Person // zero value
// Anonymous structs
config := struct {
Host string
Port int
}{
Host: "localhost",
Port: 8080,
}
Methods are functions with receivers:
// Value receiver
func (p Person) String() string {
return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}
// Pointer receiver (can modify)
func (p *Person) SetAge(age int) {
p.Age = age
}
// Method set rules
var p Person
p.SetAge(31) // Go automatically takes address
(&p).SetAge(31) // Equivalent
var ptr *Person = &p
ptr.String() // Go automatically dereferences
(*ptr).String() // Equivalent
Key Interview Points:
- Value receivers work on copies
- Pointer receivers can modify the original
- Go automatically handles pointer/value conversion for method calls
- Method sets determine interface satisfaction
Pointers Fundamentals
// Pointer basics
var x int = 42
var p *int = &x // p points to x
fmt.Println(*p) // Dereference: prints 42
*p = 100 // Modify value through pointer
// Zero value of pointer is nil
var ptr *int
if ptr == nil {
fmt.Println("ptr is nil")
}
// new() function
ptr = new(int) // Allocates zero value and returns pointer
*ptr = 42
// Pointer arithmetic is NOT allowed
// ptr++ // This is invalid in Go
Key Interview Points:
- No pointer arithmetic (safer than C/C++)
- Automatic garbage collection
- Pass by value vs pass by reference semantics
- When to use pointers vs values
Day 5-6: Functions & Interfaces
Function Types and Closures
// Function as type
type BinaryOp func(int, int) int
func add(a, b int) int { return a + b }
func multiply(a, b int) int { return a * b }
func calculate(op BinaryOp, x, y int) int {
return op(x, y)
}
// Usage
result := calculate(add, 5, 3) // 8
result = calculate(multiply, 5, 3) // 15
// Anonymous functions
square := func(x int) int {
return x * x
}
// Closures
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
Interfaces Deep Dive
Basic Interface Concepts:
// Interface definition
type Writer interface {
Write([]byte) (int, error)
}
type Reader interface {
Read([]byte) (int, error)
}
// Interface composition
type ReadWriter interface {
Reader
Writer
}
// Implementation (implicit)
type File struct {
name string
}
func (f *File) Write(data []byte) (int, error) {
// Implementation
return len(data), nil
}
func (f *File) Read(data []byte) (int, error) {
// Implementation
return len(data), nil
}
// File automatically implements Writer, Reader, and ReadWriter
Empty Interface:
// interface{} can hold any value
var i interface{}
i = 42
i = "hello"
i = []int{1, 2, 3}
// Type assertion
value, ok := i.(int)
if ok {
fmt.Printf("i is an int: %d\n", value)
}
// Type switch
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
Key Interview Points:
- Interfaces are satisfied implicitly
- Empty interface can hold any value
- Interface values have dynamic type and value
- Type assertions and type switches for type checking
- Interface segregation principle
Week 2: Concurrency & Advanced Concepts
Day 8-9: Goroutines Fundamentals
Goroutines vs Threads
// Creating goroutines
func sayHello() {
fmt.Println("Hello from goroutine!")
}
func main() {
// Start goroutine
go sayHello()
// Anonymous goroutine
go func() {
fmt.Println("Anonymous goroutine")
}()
// Goroutine with parameters
go func(name string) {
fmt.Printf("Hello, %s!\n", name)
}("World")
time.Sleep(time.Second) // Wait for goroutines
}
Key Differences from Threads:
- Lightweight (2KB initial stack vs 2MB for threads)
- Managed by Go runtime scheduler
- Multiplexed onto OS threads (M:N scheduling)
- Cheap to create (thousands or millions possible)
Race Conditions and Synchronization
Race Condition Example:
var counter int
func increment() {
for i := 0; i < 1000; i++ {
counter++ // Race condition!
}
}
func main() {
go increment()
go increment()
time.Sleep(time.Second)
fmt.Println(counter) // Unpredictable result
}
Using Mutex:
var (
counter int
mu sync.Mutex
)
func safeIncrement() {
for i := 0; i < 1000; i++ {
mu.Lock()
counter++
mu.Unlock()
}
}
Using RWMutex:
type SafeCounter struct {
mu sync.RWMutex
counters map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.counters[key]
}
WaitGroup for Synchronization:
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers completed")
}
Day 10-11: Channels Deep Dive
Channel Basics
// Channel creation
ch := make(chan int) // Unbuffered channel
buffered := make(chan int, 5) // Buffered channel
// Channel operations
ch <- 42 // Send
value := <-ch // Receive
value, ok := <-ch // Receive with ok (false if closed)
// Closing channels
close(ch)
// Channel directions (function parameters)
func send(ch chan<- int) { // Send-only channel
ch <- 42
}
func receive(ch <-chan int) { // Receive-only channel
value := <-ch
fmt.Println(value)
}
Unbuffered vs Buffered Channels
Unbuffered Channels (Synchronous):
func main() {
ch := make(chan string)
go func() {
ch <- "Hello" // Blocks until received
fmt.Println("Sent Hello")
}()
message := <-ch // Blocks until sent
fmt.Println("Received:", message)
}
Buffered Channels (Asynchronous):
func main() {
ch := make(chan string, 2)
ch <- "Hello" // Doesn't block (buffer has space)
ch <- "World" // Doesn't block (buffer has space)
// ch <- "!" // Would block (buffer full)
fmt.Println(<-ch) // "Hello"
fmt.Println(<-ch) // "World"
}
Select Statements
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(time.Second)
ch1 <- "Channel 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Channel 2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout")
return
default:
fmt.Println("No channels ready")
time.Sleep(500 * time.Millisecond)
}
}
}
Channel Patterns
Pipeline Pattern:
func generate(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
}
// Usage
numbers := generate(2, 3, 4)
squares := square(numbers)
for result := range squares {
fmt.Println(result)
}
Day 12-13: Advanced Concurrency
Context Package
// Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Context with cancellation
ctx, cancel := context.WithCancel(context.Background())
// Context with value
ctx = context.WithValue(ctx, "userID", 12345)
// Using context in functions
func doWork(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
fmt.Println("Work completed")
return nil
case <-ctx.Done():
fmt.Println("Work cancelled:", ctx.Err())
return ctx.Err()
}
}
Worker Pool Pattern
type Job struct {
ID int
Data string
}
type Result struct {
JobID int
Output string
}
func worker(id int, jobs <-chan Job, results chan<- Result) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job.ID)
time.Sleep(time.Second) // Simulate work
results <- Result{
JobID: job.ID,
Output: fmt.Sprintf("Processed: %s", job.Data),
}
}
}
func main() {
jobs := make(chan Job, 100)
results := make(chan Result, 100)
// Start workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send jobs
for j := 1; j <= 9; j++ {
jobs <- Job{ID: j, Data: fmt.Sprintf("data-%d", j)}
}
close(jobs)
// Collect results
for r := 1; r <= 9; r++ {
result := <-results
fmt.Printf("Result: %+v\n", result)
}
}
Fan-in/Fan-out Patterns
// Fan-out: Distribute work
func fanOut(in <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
ch := make(chan int)
channels[i] = ch
go func(out chan<- int) {
defer close(out)
for n := range in {
out <- n * n // Process work
}
}(ch)
}
return channels
}
// Fan-in: Merge results
func fanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan int) {
defer wg.Done()
for n := range c {
out <- n
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
sync.Once and sync.Pool
// sync.Once - Execute only once
var once sync.Once
var instance *Singleton
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
// sync.Pool - Object pooling
var pool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
func processData() {
buffer := pool.Get().([]byte)
defer pool.Put(buffer)
// Use buffer for processing
}
Week 3: Error Handling, Testing & Performance
Day 15-16: Error Handling & Best Practices
Go Error Philosophy
Go treats errors as values, not exceptions:
// Basic error handling
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Usage
result, err := divide(10, 0)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
Custom Errors
// Custom error type
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for field '%s' with value '%v': %s",
e.Field, e.Value, e.Message)
}
// Using custom errors
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Value: age,
Message: "age cannot be negative",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Message: "age seems unrealistic",
}
}
return nil
}
Error Wrapping (Go 1.13+)
func readConfig(filename string) (*Config, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return &config, nil
}
// Error checking
config, err := readConfig("app.json")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Handle file not found
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
// Handle validation error
}
}
Panic and Recover
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
if b == 0 {
panic("division by zero")
}
return a / b, nil
}
Day 17-18: Testing & Benchmarking
Unit Testing
// math.go
func Add(a, b int) int {
return a + b
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// math_test.go
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
func TestDivide(t *testing.T) {
// Test successful division
result, err := Divide(10, 2)
if err != nil {
t.Errorf("Divide(10, 2) returned error: %v", err)
}
if result != 5 {
t.Errorf("Divide(10, 2) = %d; want 5", result)
}
// Test division by zero
_, err = Divide(10, 0)
if err == nil {
t.Error("Divide(10, 0) should return error")
}
}
Table-Driven Tests
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"zero", 0, 5, 5},
{"negative result", 2, -3, -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
Benchmarking
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
func BenchmarkStringConcat(b *testing.B) {
b.Run("plus", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "hello" + "world"
}
})
b.Run("sprintf", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s%s", "hello", "world")
}
})
b.Run("builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var builder strings.Builder
builder.WriteString("hello")
builder.WriteString("world")
_ = builder.String()
}
})
}
Test Coverage and Race Detection
# Run tests with coverage
go test -cover ./...
# Generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Run tests with race detector
go test -race ./...
Mocking and Dependency Injection
// Define interface
type UserRepository interface {
GetUser(id int) (*User, error)
SaveUser(*User) error
}
// Service using the interface
type UserService struct {
repo UserRepository
}
func (s *UserService) UpdateUserEmail(id int, email string) error {
user, err := s.repo.GetUser(id)
if err != nil {
return err
}
user.Email = email
return s.repo.SaveUser(user)
}
// Mock implementation for testing
type MockUserRepository struct {
users map[int]*User
err error
}
func (m *MockUserRepository) GetUser(id int) (*User, error) {
if m.err != nil {
return nil, m.err
}
user, exists := m.users[id]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserRepository) SaveUser(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
// Test using mock
func TestUserService_UpdateUserEmail(t *testing.T) {
mockRepo := &MockUserRepository{
users: map[int]*User{
1: {ID: 1, Email: "old@example.com"},
},
}
service := &UserService{repo: mockRepo}
err := service.UpdateUserEmail(1, "new@example.com")
if err != nil {
t.Errorf("UpdateUserEmail failed: %v", err)
}
user, _ := mockRepo.GetUser(1)
if user.Email != "new@example.com" {
t.Errorf("Email not updated. Got %s, want new@example.com", user.Email)
}
}
Day 19-20: Performance & Optimization
Memory Management and GC
// Understanding allocations
func inefficientString() string {
var result string
for i := 0; i < 1000; i++ {
result += fmt.Sprintf("item %d ", i) // Creates many allocations
}
return result
}
func efficientString() string {
var builder strings.Builder
for i := 0; i < 1000; i++ {
fmt.Fprintf(&builder, "item %d ", i) // Fewer allocations
}
return builder.String()
}
// Escape analysis example
func returnsPointer() *int {
x := 42
return &x // x escapes to heap
}
func usesPointer() {
x := 42
fmt.Println(&x) // x might stay on stack
}
Profiling
// CPU profiling
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Your application code
}
// Memory profiling in tests
func BenchmarkAllocations(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
inefficientString()
}
}
Performance Best Practices
// Slice capacity management
func efficientSliceGrowth(size int) []int {
// Pre-allocate with known capacity
slice := make([]int, 0, size)
for i := 0; i < size; i++ {
slice = append(slice, i)
}
return slice
}
// String vs []byte
func processStrings(data []string) []byte {
var buffer bytes.Buffer
for _, s := range data {
buffer.WriteString(s) // More efficient than string concatenation
}
return buffer.Bytes()
}
// Avoid unnecessary allocations
func countWords(text string) map[string]int {
counts := make(map[string]int, 100) // Pre-size map if possible
// Use strings.Fields instead of regexp for simple splitting
words := strings.Fields(text)
for _, word := range words {
counts[strings.ToLower(word)]++
}
return counts
}
Day 21: Web Development Basics
HTTP Server Fundamentals
func main() {
// Basic handler
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
// Handler with method checking
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// Handle GET request
users := []string{"Alice", "Bob", "Charlie"}
json.NewEncoder(w).Encode(users)
case http.MethodPost:
// Handle POST request
var user map[string]string
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Process user creation
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Custom HTTP Handlers
type UserHandler struct {
users map[int]User
mu sync.RWMutex
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.getUsers(w, r)
case http.MethodPost:
h.createUser(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *UserHandler) getUsers(w http.ResponseWriter, r *http.Request) {
h.mu.RLock()
defer h.mu.RUnlock()
users := make([]User, 0, len(h.users))
for _, user := range h.users {
users = append(users, user)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
Middleware Pattern
type Middleware func(http.Handler) http.Handler
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Validate token
next.ServeHTTP(w, r)
})
}
// Chain middlewares
func chainMiddleware(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
// Usage
handler := &UserHandler{users: make(map[int]User)}
http.Handle("/users", chainMiddleware(handler, loggingMiddleware, authMiddleware))
Week 4: System Design, Interview Practice & Final Review
Day 22-24: Advanced Topics & System Design
Go Modules and Dependency Management
// go.mod file
module github.com/username/myproject
go 1.21
require (
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.7
)
require (
github.com/gorilla/handlers v1.5.1 // indirect
)
// Version constraints
require github.com/gin-gonic/gin v1.9.0 // Exact version
require github.com/gin-gonic/gin ^1.9.0 // Compatible version
require github.com/gin-gonic/gin ~1.9.0 // Patch level changes
Module Commands:
go mod init mymodule # Initialize module
go mod tidy # Clean up dependencies
go mod download # Download dependencies
go mod vendor # Create vendor directory
go get github.com/pkg/errors # Add dependency
go get -u ./... # Update all dependencies
Build Tags and Conditional Compilation
// file_dev.go
//go:build dev
// +build dev
package config
const DatabaseURL = "localhost:5432/myapp_dev"
// file_prod.go
//go:build prod
// +build prod
package config
const DatabaseURL = "prod-db:5432/myapp"
// file_test.go
//go:build test
// +build test
package config
const DatabaseURL = "localhost:5432/myapp_test"
Build with tags:
go build -tags dev
go build -tags prod
go test -tags test
Reflection Basics
import "reflect"
func inspectValue(v interface{}) {
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
fmt.Printf("Type: %s, Kind: %s\n", rt.Name(), rv.Kind())
// Handle different kinds
switch rv.Kind() {
case reflect.Struct:
for i := 0; i < rv.NumField(); i++ {
field := rt.Field(i)
value := rv.Field(i)
fmt.Printf("Field %s: %v\n", field.Name, value.Interface())
}
case reflect.Slice:
for i := 0; i < rv.Len(); i++ {
fmt.Printf("Element %d: %v\n", i, rv.Index(i).Interface())
}
}
}
// JSON-like marshaling with reflection
func marshalStruct(v interface{}) map[string]interface{} {
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
if rv.Kind() != reflect.Struct {
return nil
}
result := make(map[string]interface{})
for i := 0; i < rv.NumField(); i++ {
field := rt.Field(i)
if field.IsExported() { // Only exported fields
result[field.Name] = rv.Field(i).Interface()
}
}
return result
}
When to Use/Avoid Reflection:
- Use: Generic libraries, serialization, ORMs
- Avoid: Performance-critical code, when type safety is important
- Alternative: Code generation for better performance
System Design with Go
Microservices Architecture:
// Service discovery interface
type ServiceRegistry interface {
Register(service *ServiceInfo) error
Discover(serviceName string) ([]*ServiceInfo, error)
Deregister(serviceID string) error
}
type ServiceInfo struct {
ID string
Name string
Host string
Port int
Metadata map[string]string
}
// Circuit breaker pattern
type CircuitBreaker struct {
maxFailures int
timeout time.Duration
failures int
lastFailTime time.Time
state CircuitState
mu sync.RWMutex
}
type CircuitState int
const (
Closed CircuitState = iota
Open
HalfOpen
)
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
defer cb.mu.Unlock()
if cb.state == Open {
if time.Since(cb.lastFailTime) > cb.timeout {
cb.state = HalfOpen
} else {
return errors.New("circuit breaker is open")
}
}
err := fn()
if err != nil {
cb.failures++
cb.lastFailTime = time.Now()
if cb.failures >= cb.maxFailures {
cb.state = Open
}
return err
}
// Success - reset circuit breaker
cb.failures = 0
cb.state = Closed
return nil
}
Load Balancer Implementation:
type LoadBalancer interface {
NextServer() *Server
AddServer(*Server)
RemoveServer(serverID string)
}
// Round-robin load balancer
type RoundRobinLB struct {
servers []*Server
current int
mu sync.Mutex
}
func (lb *RoundRobinLB) NextServer() *Server {
lb.mu.Lock()
defer lb.mu.Unlock()
if len(lb.servers) == 0 {
return nil
}
server := lb.servers[lb.current]
lb.current = (lb.current + 1) % len(lb.servers)
return server
}
// Weighted round-robin
type WeightedServer struct {
*Server
Weight int
CurrentWeight int
}
type WeightedRoundRobinLB struct {
servers []*WeightedServer
mu sync.Mutex
}
func (lb *WeightedRoundRobinLB) NextServer() *Server {
lb.mu.Lock()
defer lb.mu.Unlock()
if len(lb.servers) == 0 {
return nil
}
totalWeight := 0
var selected *WeightedServer
for _, server := range lb.servers {
server.CurrentWeight += server.Weight
totalWeight += server.Weight
if selected == nil || server.CurrentWeight > selected.CurrentWeight {
selected = server
}
}
selected.CurrentWeight -= totalWeight
return selected.Server
}
Database Integration Patterns
// Repository pattern
type UserRepository interface {
Create(user *User) error
GetByID(id int) (*User, error)
Update(user *User) error
Delete(id int) error
List(limit, offset int) ([]*User, error)
}
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) Create(user *User) error {
query := `INSERT INTO users (name, email, created_at) VALUES ($1, $2, $3) RETURNING id`
err := r.db.QueryRow(query, user.Name, user.Email, time.Now()).Scan(&user.ID)
return err
}
func (r *PostgresUserRepository) GetByID(id int) (*User, error) {
user := &User{}
query := `SELECT id, name, email, created_at FROM users WHERE id = $1`
err := r.db.QueryRow(query, id).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err == sql.ErrNoRows {
return nil, ErrUserNotFound
}
return user, err
}
// Transaction management
func (r *PostgresUserRepository) TransferMoney(fromID, toID int, amount decimal.Decimal) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Debit from source account
_, err = tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID)
if err != nil {
return err
}
// Credit to destination account
_, err = tx.Exec("UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, toID)
if err != nil {
return err
}
return tx.Commit()
}
Day 25-27: Mock Interviews & Coding Practice
Common Go Interview Questions with Detailed Answers
1. “Explain the difference between arrays and slices in Go.”
Answer Structure:
- Definition and syntax differences
- Memory layout and performance implications
- Use cases for each
- Code examples
2. “How does Go’s garbage collector work?”
Answer Points:
- Concurrent, tri-color mark-and-sweep collector
- Low-latency design
- How it affects application performance
- Tuning GC with GOGC environment variable
3. “Explain Go’s interface system and why it’s different.”
Answer Structure:
- Implicit satisfaction vs explicit implementation
- Interface values (type and value)
- Empty interface and type assertions
- Why it enables better composition
4. “What are goroutines and how do they differ from threads?”
Answer Points:
- Lightweight threading model
- Go scheduler (M:N model)
- Stack management (segmented stacks)
- Performance characteristics
System Design Questions
“Design a URL shortener like bit.ly using Go”
Key Components to Discuss:
// Core service structure
type URLShortener struct {
storage Storage
cache Cache
generator IDGenerator
stats StatsCollector
}
// Storage interface for different backends
type Storage interface {
Store(shortID string, originalURL string, expiry time.Time) error
Retrieve(shortID string) (string, error)
Delete(shortID string) error
}
// Caching layer
type Cache interface {
Get(key string) (string, bool)
Set(key string, value string, ttl time.Duration) error
Delete(key string) error
}
// ID generation strategies
type IDGenerator interface {
Generate() string
}
// Base62 encoder for short IDs
type Base62Generator struct {
counter int64
}
func (g *Base62Generator) Generate() string {
id := atomic.AddInt64(&g.counter, 1)
return encodeBase62(id)
}
Architecture Considerations:
- Horizontal scaling with consistent hashing
- Database sharding strategies
- Caching layers (Redis/Memcached)
- Rate limiting and abuse prevention
- Analytics and monitoring
Coding Problem Patterns
Concurrency Problems:
// Producer-consumer with bounded buffer
func ProducerConsumer(bufferSize int) {
buffer := make(chan int, bufferSize)
var wg sync.WaitGroup
// Producer
wg.Add(1)
go func() {
defer wg.Done()
defer close(buffer)
for i := 0; i < 10; i++ {
buffer <- i
fmt.Printf("Produced: %d\n", i)
}
}()
// Consumer
wg.Add(1)
go func() {
defer wg.Done()
for item := range buffer {
fmt.Printf("Consumed: %d\n", item)
time.Sleep(100 * time.Millisecond)
}
}()
wg.Wait()
}
// Rate limiter implementation
type RateLimiter struct {
tokens chan struct{}
ticker *time.Ticker
done chan struct{}
}
func NewRateLimiter(rate int, burst int) *RateLimiter {
rl := &RateLimiter{
tokens: make(chan struct{}, burst),
ticker: time.NewTicker(time.Second / time.Duration(rate)),
done: make(chan struct{}),
}
// Fill initial burst
for i := 0; i < burst; i++ {
rl.tokens <- struct{}{}
}
go rl.run()
return rl
}
func (rl *RateLimiter) run() {
for {
select {
case <-rl.ticker.C:
select {
case rl.tokens <- struct{}{}:
default: // Bucket full
}
case <-rl.done:
rl.ticker.Stop()
return
}
}
}
func (rl *RateLimiter) Allow() bool {
select {
case <-rl.tokens:
return true
default:
return false
}
}
Day 28-29: Final Review & Weak Areas
Go Memory Model Key Points
// Happens-before relationships
var a string
var done bool
func setup() {
a = "hello, world" // Write to a
done = true // Write to done
}
func main() {
go setup()
for !done { // Read from done
// busy wait
}
print(a) // Read from a - guaranteed to see "hello, world"
}
// Channel synchronization
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world"
c <- 0
}
func main() {
go f()
<-c
print(a) // Guaranteed to print "hello, world"
}
Performance Optimization Checklist
// Efficient string building
func BuildString(parts []string) string {
var builder strings.Builder
builder.Grow(estimateSize(parts)) // Pre-allocate capacity
for _, part := range parts {
builder.WriteString(part)
}
return builder.String()
}
// Slice pre-allocation
func ProcessItems(items []Item) []Result {
results := make([]Result, 0, len(items)) // Pre-allocate capacity
for _, item := range items {
if result := process(item); result.IsValid() {
results = append(results, result)
}
}
return results
}
// Map pre-sizing
func CountWords(text string) map[string]int {
words := strings.Fields(text)
counts := make(map[string]int, len(words)/2) // Estimate capacity
for _, word := range words {
counts[word]++
}
return counts
}
// Avoiding allocations in hot paths
func FastPath(data []byte) bool {
// Use byte operations instead of string conversions
return bytes.HasPrefix(data, []byte("prefix"))
}
Common Gotchas and Best Practices
// Goroutine leaks
func BadExample() {
ch := make(chan int)
go func() {
ch <- 42 // This goroutine will leak if nobody reads from ch
}()
// Channel never read from - goroutine leaks
}
func GoodExample() {
ch := make(chan int, 1) // Buffered channel prevents leak
go func() {
ch <- 42
}()
// Or ensure someone reads from the channel
}
// Loop variable capture
func BadLoopExample() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // Prints 3, 3, 3 (or unpredictable)
}()
}
}
func GoodLoopExample() {
for i := 0; i < 3; i++ {
go func(val int) {
fmt.Println(val) // Prints 0, 1, 2 (in some order)
}(i)
}
}
// Slice modification during iteration
func BadSliceExample(slice []int) {
for i, v := range slice {
if v == 0 {
slice = append(slice[:i], slice[i+1:]...) // Dangerous!
}
}
}
func GoodSliceExample(slice []int) []int {
var result []int
for _, v := range slice {
if v != 0 {
result = append(result, v)
}
}
return result
}
Day 30: Final Preparation
Quick Reference - Core Concepts
Data Types:
- Basic types: int, float64, string, bool
- Composite types: array, slice, map, struct, pointer, function, interface, channel
- Zero values: 0, “”, false, nil
Control Flow:
- if/else, for (only loop), switch, select
- defer, panic, recover
Concurrency:
- goroutines:
go func()
- channels:
make(chan Type, capacity)
- select: non-blocking channel operations
- sync package: Mutex, WaitGroup, Once
Interfaces:
- Implicit satisfaction
- Empty interface:
interface{}
- Type assertions:
value.(Type)
Error Handling:
- Errors as values
- Multiple return values:
value, err
- Error wrapping:
fmt.Errorf("...: %w", err)
Last-Minute Interview Tips
Technical Communication:
- Think out loud - explain your reasoning
- Start with brute force, then optimize
- Consider edge cases and error conditions
- Discuss trade-offs between solutions
Go-Specific Points to Mention:
- Why Go? Performance, simplicity, concurrency
- Goroutines vs threads - lightweight, managed by runtime
- Interfaces enable composition over inheritance
- Garbage collected but performance-oriented
- Strong ecosystem and tooling
Questions to Ask Interviewer:
- How does your team use Go’s concurrency features?
- What Go libraries/frameworks do you use?
- How do you handle microservices communication?
- What’s your testing and deployment strategy?
- How do you monitor Go applications in production?
Final Confidence Boosters
Remember these Go strengths:
- Simple syntax with powerful features
- Excellent concurrency primitives
- Fast compilation and execution
- Strong standard library
- Great tooling (go fmt, go test, go mod)
- Used by major companies (Google, Uber, Docker, Kubernetes)
You’ve covered all the essential topics. Trust your preparation and focus on clear communication during the interview. Good luck!