GO 31 to 40

Chapter 31 — REST API Development

Question

Given below is a code snippet that:

  • Creates an HTTP server.

  • Defines a REST-style endpoint for retrieving a user.

  • Returns JSON with an HTTP status code.

  • Demonstrates how a handler processes a request.

  • Shows the difference between a successful response and a missing resource.

What should be the output of the following code?

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
)

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func getUser(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")

	if id != "1" {
		http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)

	json.NewEncoder(w).Encode(User{ID: 1, Name: "Go Developer"})
}

func main() {
	server := http.HandlerFunc(getUser)

	for _, id := range []string{"1", "2"} {
		req := httptest.NewRequest(http.MethodGet, "/users?id="+id, nil)
		rec := httptest.NewRecorder()

		server.ServeHTTP(rec, req)

		fmt.Println(rec.Code)
		fmt.Print(rec.Body.String())
	}
}

Answer

200
{"id":1,"name":"Go Developer"}
404
{"error":"user not found"}

Step-by-step explanation

  1. httptest.NewRequest creates a test HTTP request. It does not send a real network request.

  2. httptest.NewRecorder captures the response so we can inspect it.

  3. server.ServeHTTP(rec, req) runs the handler.

  4. For id=1, the handler writes status 200 and encodes the user as JSON.

  5. json.NewEncoder(w).Encode(...) converts the struct into JSON and adds a newline.

  6. For id=2, the condition is true, so http.Error writes the error response and status 404.

  7. return stops the handler immediately.

How to read the important code

server.ServeHTTP(rec, req)

"server dot ServeHTTP, open parenthesis, rec comma req, close parenthesis."

This means: run the HTTP handler using this request and record the response.

json.NewEncoder(w).Encode(user)

"json dot NewEncoder, open parenthesis, w, close parenthesis, dot Encode, open parenthesis, user, close parenthesis."

This means: create a JSON encoder that writes to the response, then encode the user.

Key takeaway

A REST API handler receives a request, decides what to return, and writes a response with a status code and data.


Chapter 32 — Middleware

Question

Given below is a code snippet that:

  • Creates middleware that runs before and after a handler.

  • Demonstrates how middleware wraps another handler.

  • Shows execution order.

  • Uses a request ID to identify a request.

  • Demonstrates how middleware can modify a response.

What should be the output of the following code?

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

func logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("before handler")

		next.ServeHTTP(w, r)

		fmt.Println("after handler")
	})
}

func addRequestID(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Request-ID", "abc123")
		next.ServeHTTP(w, r)
	})
}

func main() {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("inside handler")
		fmt.Fprintln(w, "Hello")
	})

	handler = logging(addRequestID(handler))

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)

	fmt.Println("response:", rec.Body.String())
	fmt.Println("request ID:", rec.Header().Get("X-Request-ID"))
}

Answer

before handler
inside handler
after handler
response: Hello

request ID: abc123

Step-by-step explanation

  1. addRequestID(handler) wraps the original handler.

  2. logging(...) wraps that result.

  3. The outer logging middleware runs first and prints before handler.

  4. It calls the next middleware.

  5. The request ID middleware sets the response header and calls the original handler.

  6. The original handler prints inside handler and writes Hello.

  7. Control returns to the logging middleware, which prints after handler.

  8. rec.Body.String() contains "Hello\n" because fmt.Fprintln adds a newline.

How to read the important code

func logging(next http.Handler) http.Handler

"func logging, open parenthesis, next http dot Handler, close parenthesis, http dot Handler."

This means: logging receives a handler and returns another handler.

handler = logging(addRequestID(handler))

"handler equals logging, open parenthesis, addRequestID, open parenthesis, handler, close parenthesis, close parenthesis."

This means: wrap the handler with request-ID middleware, then wrap that with logging middleware.

Key takeaway

Middleware is a handler that adds behavior before or after another handler.


Chapter 33 — Routing & Request Handling

Question

Given below is a code snippet that:

  • Routes different HTTP methods to different handlers.

  • Demonstrates path parameters using Go's standard library router.

  • Returns 405 Method Not Allowed for an unsupported method.

  • Shows how a handler reads a path parameter.

  • Demonstrates how routing selects the correct handler.

What should be the output of the following code?

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "GET user:", r.PathValue("id"))
	})

	mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "POST user")
	})

	for _, test := range []struct {
		method string
		path   string
	}{
		{http.MethodGet, "/users/42"},
		{http.MethodPost, "/users"},
		{http.MethodDelete, "/users/42"},
	} {
		req := httptest.NewRequest(test.method, test.path, nil)
		rec := httptest.NewRecorder()

		mux.ServeHTTP(rec, req)

		fmt.Println(test.method, test.path, rec.Code)
		fmt.Print(rec.Body.String())
	}
}

Answer

GET /users/42 200
GET user: 42
POST /users 200
POST user
DELETE /users/42 405
Method Not Allowed

Step-by-step explanation

  1. http.NewServeMux() creates a router.

  2. "GET /users/{id}" matches GET requests whose path begins with /users/.

  3. {id} is a path wildcard.

  4. r.PathValue("id") retrieves the value captured by that wildcard.

  5. "POST /users" matches only POST requests to /users.

  6. The DELETE request does not match an allowed method for that route, so the router returns 405.

  7. rec.Code records the HTTP status code.

How to read the important code

mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {

"mux dot HandleFunc, open parenthesis, double quote GET slash users slash curly brace id curly brace double quote, comma, func..."

This means: register an anonymous function to handle GET requests matching /users/{id}.

r.PathValue("id")

"r dot PathValue, open parenthesis, double quote id double quote, close parenthesis."

This means: retrieve the value captured by the {id} path parameter.

Key takeaway

A router selects a handler based on the HTTP method and URL path.


Chapter 34 — Goroutines

Question

Given below is a code snippet that:

  • Starts a goroutine.

  • Demonstrates that a goroutine runs concurrently with the main function.

  • Uses a channel to wait for completion.

  • Shows how execution order can differ from source-code order.

  • Demonstrates how a channel can signal that work is finished.

What should be the output of the following code?

package main

import "fmt"

func main() {
	done := make(chan bool)

	go func() {
		fmt.Println("worker: started")
		fmt.Println("worker: finished")
		done <- true
	}()

	fmt.Println("main: waiting")
	<-done
	fmt.Println("main: finished")
}

Answer

main: waiting
worker: started
worker: finished
main: finished

Step-by-step explanation

  1. make(chan bool) creates a channel that carries boolean values.

  2. go func() { ... }() starts the anonymous function in a goroutine.

  3. The main function prints main: waiting.

  4. The worker prints its two messages.

  5. done <- true sends true into the channel.

  6. <-done receives the value and allows the main function to continue.

  7. The main function prints main: finished.

How to read the important code

go func() {

"go func, open parentheses, open curly brace."

This means: start this function in a new goroutine.

done <- true

"done channel, send true."

The <- operator sends a value into a channel.

<-done

"receive from done."

The <- operator receives a value from a channel.

Key takeaway

A goroutine runs concurrently, and a channel can be used to wait for its result.


Chapter 35 — Channels

Question

Given below is a code snippet that:

  • Creates a channel.

  • Sends a value through the channel.

  • Receives the value in another goroutine.

  • Demonstrates that an unbuffered channel synchronizes sender and receiver.

  • Shows the order in which the messages are printed.

What should be the output of the following code?

package main

import "fmt"

func main() {
	ch := make(chan int)

	go func() {
		fmt.Println("worker: before send")
		ch <- 42
		fmt.Println("worker: after send")
	}()

	fmt.Println("main: before receive")
	value := <-ch
	fmt.Println("main: received", value)
}

Answer

main: before receive
worker: before send
main: received 42
worker: after send

Step-by-step explanation

  1. make(chan int) creates an unbuffered channel.

  2. The goroutine prints worker: before send.

  3. ch <- 42 attempts to send 42.

  4. Because the channel is unbuffered, the send waits until another goroutine receives the value.

  5. The main function receives the value with <-ch.

  6. The receive completes, so the main function prints main: received 42.

  7. The worker continues and prints worker: after send.

How to read the important code

ch := make(chan int)

"ch colon equals make, open parenthesis, chan int, close parenthesis."

This creates an unbuffered channel that carries integers.

value := <-ch

"value colon equals receive from ch."

This receives a value from the channel and stores it in value.

Key takeaway

An unbuffered channel makes the sender and receiver wait for each other.


Chapter 36 — select

Question

Given below is a code snippet that:

  • Creates two channels.

  • Sends values through goroutines.

  • Uses select to wait for whichever channel is ready.

  • Demonstrates how select chooses a ready communication.

  • Shows how a timeout can be used to prevent waiting forever.

What should be the output of the following code?

package main

import (
	"fmt"
	"time"
)

func main() {
	fast := make(chan string)
	slow := make(chan string)

	go func() {
		time.Sleep(10 * time.Millisecond)
		fast <- "fast result"
	}()

	go func() {
		time.Sleep(30 * time.Millisecond)
		slow <- "slow result"
	}()

	select {
	case result := <-fast:
		fmt.Println(result)
	case result := <-slow:
		fmt.Println(result)
	case <-time.After(100 * time.Millisecond):
		fmt.Println("timeout")
	}
}

Answer

fast result

Step-by-step explanation

  1. fast and slow are channels that carry strings.

  2. The first goroutine sends "fast result" after 10 milliseconds.

  3. The second goroutine sends "slow result" after 30 milliseconds.

  4. select waits until one of its communication cases is ready.

  5. The fast channel becomes ready first.

  6. The first case runs, so the program prints fast result.

  7. The timeout case does not run because a result arrived before 100 milliseconds.

How to read the important code

select {
case result := <-fast:

"select, open curly brace, case result colon equals receive from fast, colon."

This means: wait until receiving from fast is possible, then execute this case.

case <-time.After(100 * time.Millisecond):

"case receive from time dot After, open parenthesis, one hundred times time dot Millisecond, close parenthesis, colon."

This means: if 100 milliseconds pass before another case is ready, execute the timeout case.

Key takeaway

select lets a goroutine wait for multiple channel operations and respond to whichever becomes ready first.


Chapter 37 — Mutexes, WaitGroups & Synchronization

Question

Given below is a code snippet that:

  • Starts multiple goroutines.

  • Uses a mutex to protect shared data.

  • Uses a WaitGroup to wait for all goroutines to finish.

  • Demonstrates why shared data must be synchronized.

  • Shows the final value after all increments are complete.

What should be the output of the following code?

package main

import (
	"fmt"
	"sync"
)

func main() {
	var (
		mu      sync.Mutex
		wg      sync.WaitGroup
		counter int
	)

	for i := 0; i < 3; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()

			for j := 0; j < 1000; j++ {
				mu.Lock()
				counter++
				mu.Unlock()
			}
		}()
	}

	wg.Wait()
	fmt.Println(counter)
}

Answer

3000

Step-by-step explanation

  1. counter is shared by all three goroutines.

  2. Each goroutine increments it 1,000 times.

  3. mu.Lock() allows only one goroutine at a time to access the protected operation.

  4. counter++ reads the current value and increases it by one.

  5. mu.Unlock() allows another goroutine to enter.

  6. wg.Add(1) tells the WaitGroup that one goroutine has started.

  7. defer wg.Done() marks that goroutine as finished when it returns.

  8. wg.Wait() blocks until all three goroutines finish.

  9. Therefore, the final value is 3 × 1000 = 3000.

How to read the important code

var mu sync.Mutex

"var mu sync dot Mutex."

A mutex is a lock that allows only one goroutine at a time to enter a protected section of code.

defer wg.Done()

"defer wg dot Done, open parentheses, close parentheses."

This means: when the current function finishes, call wg.Done().

Key takeaway

A mutex protects shared data, and a WaitGroup waits for multiple goroutines to finish.


Chapter 38 — Context & Cancellation

Question

Given below is a code snippet that:

  • Creates a cancellable context.

  • Starts a goroutine that checks for cancellation.

  • Demonstrates how context.Context communicates cancellation.

  • Uses select to wait for work or cancellation.

  • Shows how a function can stop early when its context is cancelled.

What should be the output of the following code?

package main

import (
	"context"
	"fmt"
	"time"
)

func worker(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("worker: cancelled")
			return
		default:
			fmt.Println("worker: working")
			time.Sleep(10 * time.Millisecond)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	go worker(ctx)

	time.Sleep(25 * time.Millisecond)
	cancel()

	time.Sleep(10 * time.Millisecond)
	fmt.Println("main: finished")
}

Answer

worker: working
worker: working
worker: working
worker: cancelled
main: finished

Step-by-step explanation

  1. context.Background() creates a root context.

  2. context.WithCancel creates a child context and a cancellation function.

  3. The worker starts and prints worker: working.

  4. It repeats until the context is cancelled.

  5. After approximately 25 milliseconds, cancel() signals cancellation.

  6. The worker receives from ctx.Done() and prints worker: cancelled.

  7. return stops the worker.

  8. The main function prints main: finished.

How to read the important code

ctx, cancel := context.WithCancel(context.Background())

"ctx comma cancel colon equals context dot WithCancel, open parenthesis, context dot Background, open parentheses, close parentheses, close parenthesis."

This creates a context and a function that can cancel it.

case <-ctx.Done():

"case receive from ctx dot Done, open parentheses, close parentheses, colon."

This means: if the context is cancelled, execute this case.

Key takeaway

Context cancellation lets a function stop work when its caller no longer needs the result.


Chapter 39 — Concurrency Patterns

Question

Given below is a code snippet that:

  • Demonstrates a worker-pool pattern.

  • Sends jobs through a channel.

  • Uses multiple workers to process jobs.

  • Uses a WaitGroup to wait for all workers.

  • Uses a separate goroutine to close the results channel.

  • Shows how concurrent work can be coordinated.

What should be the output of the following code?

package main

import (
	"fmt"
	"sync"
)

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

	var wg sync.WaitGroup

	worker := func() {
		defer wg.Done()

		for job := range jobs {
			results <- job * 2
		}
	}

	wg.Add(2)
	go worker()
	go worker()

	go func() {
		for i := 1; i <= 4; i++ {
			jobs <- i
		}
		close(jobs)
	}()

	go func() {
		wg.Wait()
		close(results)
	}()

	for result := range results {
		fmt.Println(result)
	}
}

Answer

2
4
6
8

Step-by-step explanation

  1. jobs carries integers to workers.

  2. results carries processed values back to the main function.

  3. Two workers run concurrently.

  4. Each worker receives jobs from jobs using range.

  5. Each job is multiplied by two and sent to results.

  6. The producer sends 1, 2, 3, and 4, then closes jobs.

  7. Closing jobs tells workers that no more jobs will arrive.

  8. Each worker finishes after processing all jobs.

  9. wg.Wait() waits for both workers to finish.

  10. close(results) tells the main function that no more results will arrive.

  11. The main function ranges over results and prints each value.

  12. The exact order is not guaranteed in general, but these four values are produced.

How to read the important code

for job := range jobs {

"for job colon equals range jobs, open curly brace."

This means: receive values from the jobs channel until it is closed.

close(results)

"close, open parenthesis, results, close parenthesis."

This means: signal that no more values will be sent on the results channel.

Key takeaway

A worker pool divides jobs among goroutines and uses channels to coordinate work and results.


Chapter 40 — Race Conditions & Race Detector

Question

Given below is a code snippet that:

  • Demonstrates a shared variable accessed by multiple goroutines.

  • Shows why unsynchronized access is unsafe.

  • Demonstrates how to fix the race with a mutex.

  • Shows the difference between a race-free program and a race-prone program.

  • Teaches the purpose of Go's race detector.

What should be the output of the following code?

package main

import (
	"fmt"
	"sync"
)

func main() {
	var (
		mu      sync.Mutex
		wg      sync.WaitGroup
		counter int
	)

	for i := 0; i < 5; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()

			for j := 0; j < 1000; j++ {
				mu.Lock()
				counter++
				mu.Unlock()
			}
		}()
	}

	wg.Wait()
	fmt.Println("counter:", counter)
}

Answer

counter: 5000

Step-by-step explanation

  1. Five goroutines share the same counter.

  2. Each goroutine increments it 1,000 times.

  3. Without synchronization, two goroutines could read the same old value and overwrite each other's updates.

  4. That problem is called a race condition: the result depends on the timing of concurrent operations.

  5. The mutex ensures that only one goroutine modifies counter at a time.

  6. Every increment is therefore preserved.

  7. wg.Wait() ensures all goroutines finish before the final print.

  8. The final value is 5 × 1000 = 5000.

  9. Go's race detector can detect unsynchronized conflicting accesses when the program is run with go run -race or go test -race.

How to read the important code

go run -race main.go

"go run dash race main dot go."

This runs the program with Go's race detector, which checks for certain unsafe concurrent memory accesses.

mu.Lock()
counter++
mu.Unlock()

"mu dot Lock, open parentheses, close parentheses. counter plus plus. mu dot Unlock, open parentheses, close parentheses."

This means: lock the shared data, update it, then unlock it.

Key takeaway

A race condition can corrupt shared data, and Go's race detector helps find unsafe concurrent access.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.