GoLang Interview Questions with Answers
Most Asked GoLang Interview Questions for Software Engineer Roles
Introduction
This page provides a complete collection of Go Interview Questions and Answers designed for Go developers, backend engineers, cloud engineers, and software professionals preparing for technical interviews. Go (often referred to as Golang) is an open‑source, statically typed, compiled programming language developed by Google. It is designed for simplicity, performance, and scalability, making it a top choice for building cloud‑native applications, microservices, APIs, web servers, and distributed systems. This interview guide covers beginner, intermediate, and advanced Go concepts including syntax, variables, data types, functions, structs, interfaces, pointers, concurrency (goroutines and channels), error handling, packages, testing, and real‑world Go development scenarios.
Why Go?
- Simple, readable syntax – easy to learn and maintain
- Built‑in concurrency – goroutines and channels for high‑performance parallel processing
- Compiles to a single binary – fast execution and simple deployment
- Garbage collection – automatic memory management with low latency
- Rich standard library – includes HTTP, crypto, testing, and more
- Used by companies like Google, Uber, Dropbox, and Kubernetes
- High demand – one of the most sought‑after skills in backend and cloud roles
Most Asked Go Interview Questions
Go (or Golang) is a statically typed, compiled programming language designed at Google. It combines the performance of C with the productivity of modern languages.
- Concurrency: Goroutines and channels
- Simplicity: Clean syntax, easy to learn
- Performance: Compiled to machine code
- Garbage Collection: Automatic memory management
- Strong Standard Library: HTTP, crypto, testing
// Hello World in Go
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}Go provides a rich set of built-in data types including basic types, composite types, and reference types. All types are statically typed.
- Basic Types: int, float64, bool, string, rune, byte
- Composite Types: array, slice, map, struct
- Reference Types: pointer, slice, map, channel
- Interface: Defines behavior
- Type Inference: Use
:=for implicit typing
// Data Types in Go
package main
import "fmt"
func main() {
var age int = 25
var salary float64 = 50000.50
var pi float64 = 3.14159265358979
var grade rune = 'A'
var isActive bool = true
var name string = "Alice"
var price float64 = 99.99
fmt.Printf("Age: %d\n", age)
fmt.Printf("Salary: %.2f\n", salary)
fmt.Printf("Pi: %f\n", pi)
fmt.Printf("Grade: %c\n", grade)
fmt.Printf("Active: %t\n", isActive)
fmt.Printf("Name: %s\n", name)
fmt.Printf("Price: %.2f\n", price)
}Go uses var for variables and const for constants. Variables can be declared with explicit types or using type inference with :=.
- var: Explicit variable declaration
- :=: Short variable declaration with type inference
- const: Compile-time constants
- Multiple Declarations:
var x, y int = 1, 2 - Block Declarations: Group related declarations
// Variables and Constants in Go
package main
import "fmt"
func main() {
var x int = 10
const PI float64 = 3.14159
val := 3.14
str := "Hello"
var counter int = 0
fmt.Printf("x = %d\n", x)
fmt.Printf("PI = %f\n", PI)
fmt.Printf("val = %f\n", val)
fmt.Printf("str = %s\n", str)
fmt.Printf("counter = %d\n", counter)
}Arrays are fixed-size sequences. Slices are dynamic, flexible views into arrays. Slices are more common in Go programming.
- Array: Fixed size, value type
- Slice: Dynamic size, reference type
- Make: Create slices with
make([]T, len, cap) - Append:
append(slice, elements...) - Slice Operations:
slice[low:high]
// Arrays and Slices in Go
package main
import "fmt"
func main() {
// Array (fixed size)
var arr [5]int = [5]int{1, 2, 3, 4, 5}
fmt.Printf("arr[0] = %d\n", arr[0])
fmt.Printf("arr[2] = %d\n", arr[2])
// Slice (dynamic size)
slice := []int{1, 2, 3, 4, 5}
slice = append(slice, 6, 7)
fmt.Printf("Slice: %v\n", slice)
// Slice operations
slice2 := slice[1:4]
fmt.Printf("Slice2: %v\n", slice2)
// 2D Slice
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
fmt.Printf("matrix[1][1] = %d\n", matrix[1][1])
}Functions are first-class citizens in Go. They can return multiple values, be assigned to variables, and be passed as arguments.
- Function Declaration:
func name(params) returnType - Multiple Returns:
func() (int, error) - Named Returns: Named return values
- Variadic Functions:
func sum(nums ...int) - Anonymous Functions: Functions without a name
// Functions in Go
package main
import "fmt"
// Basic function
func add(a int, b int) int {
return a + b
}
// Function with multiple return values
func divide(a, b int) (int, int) {
return a / b, a % b
}
// Function with named return values
func getMinMax(nums []int) (min int, max int) {
min = nums[0]
max = nums[0]
for _, v := range nums {
if v < min {
min = v
}
if v > max {
max = v
}
}
return
}
// Variadic function
func sum(nums ...int) int {
total := 0
for _, v := range nums {
total += v
}
return total
}
func main() {
fmt.Printf("Add: %d\n", add(10, 20))
quotient, remainder := divide(20, 10)
fmt.Printf("Quotient: %d, Remainder: %d\n", quotient, remainder)
min, max := getMinMax([]int{5, 2, 8, 1, 9})
fmt.Printf("Min: %d, Max: %d\n", min, max)
fmt.Printf("Sum: %d\n", sum(1, 2, 3, 4, 5))
}Recursion is a technique where a function calls itself. Go supports recursion with proper base cases and stack management.
- Base Case: Stopping condition
- Recursive Case: Self-call with smaller input
- Stack Depth: Be mindful of recursion depth
- Tail Recursion: Go does not optimize tail recursion
- Use Cases: Tree traversal, factorial, Fibonacci
// Recursion in Go
package main
import "fmt"
// Factorial
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
// Fibonacci
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
// Sum of array
func sumArray(arr []int, n int) int {
if n <= 0 {
return 0
}
return arr[n-1] + sumArray(arr, n-1)
}
func main() {
fmt.Printf("Factorial 5: %d\n", factorial(5))
fmt.Printf("Fibonacci 8: %d\n", fibonacci(8))
fmt.Printf("Sum [1,2,3,4,5]: %d\n", sumArray([]int{1, 2, 3, 4, 5}, 5))
}Maps are key-value pairs that provide fast lookups. Keys can be any comparable type, and values can be any type.
- Declaration:
map[keyType]valueType - Make:
make(map[string]int) - Access:
value := map["key"] - Check Existence:
value, ok := map["key"] - Delete:
delete(map, "key")
// Maps in Go
package main
import "fmt"
func main() {
// Map declaration
scores := map[string]int{
"Alice": 95,
"Bob": 87,
"Carol": 92,
}
// Access values
fmt.Printf("Alice: %d\n", scores["Alice"])
fmt.Printf("Bob: %d\n", scores["Bob"])
// Add new key-value
scores["Dave"] = 88
// Check if key exists
val, exists := scores["Eve"]
if exists {
fmt.Printf("Eve: %d\n", val)
} else {
fmt.Println("Eve not found")
}
// Iterate map
for key, value := range scores {
fmt.Printf("%s: %d\n", key, value)
}
// Delete key
delete(scores, "Bob")
fmt.Printf("After delete: %v\n", scores)
}Structs are composite data types that group fields together. They are the main way to define custom types in Go.
- Declaration:
type Person struct { Name string } - Initialization:
Person{Name: "Alice"} - Methods: Functions with receiver
- Pointer Receivers: Modify struct in methods
- Embedding: Compose structs
// Structs in Go
package main
import "fmt"
// Struct declaration
type Person struct {
Name string
Age int
Email string
}
// Method with value receiver
func (p Person) Greet() string {
return fmt.Sprintf("Hello, I'm %s", p.Name)
}
// Method with pointer receiver (modifies struct)
func (p *Person) UpdateEmail(newEmail string) {
p.Email = newEmail
}
// Method with pointer receiver for modification
func (p *Person) HaveBirthday() {
p.Age++
}
// Struct embedding (composition)
type Employee struct {
Person // Embedded struct
EmployeeID int
Department string
}
func main() {
// Declaration and initialization
person1 := Person{Name: "Alice", Age: 25, Email: "alice@email.com"}
person2 := Person{Name: "Bob", Age: 30, Email: "bob@email.com"}
// Using methods
fmt.Println(person1.Greet())
person1.UpdateEmail("alice@new.com")
fmt.Printf("Updated email: %s\n", person1.Email)
// Pointer receiver modification
fmt.Printf("Bob's age: %d\n", person2.Age)
person2.HaveBirthday()
fmt.Printf("Bob's age after birthday: %d\n", person2.Age)
// Struct embedding
employee := Employee{
Person: Person{Name: "Charlie", Age: 28, Email: "charlie@company.com"},
EmployeeID: 1001,
Department: "Engineering",
}
fmt.Printf("Employee: %s, ID: %d, Dept: %s\n",
employee.Name, employee.EmployeeID, employee.Department)
}Interfaces define behavior through method signatures. They enable polymorphism and decoupling in Go programs.
- Declaration:
type Reader interface { Read([]byte) int } - Implementation: Implicit implementation
- Empty Interface:
interface{} - Type Assertion:
value, ok := interface.(Type) - Interface Composition: Combining interfaces
// Interfaces in Go
package main
import "fmt"
import "math"
// Interface definition
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle implementation
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
}
// Rectangle implementation
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Interface as parameter
func printShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
shapes := []Shape{
Circle{Radius: 5.0},
Rectangle{Width: 4.0, Height: 6.0},
}
for _, s := range shapes {
printShapeInfo(s)
}
}Pointers store the memory address of a value. They are used for efficiency and to modify values in functions.
- Declaration:
var p *int - Address Operator:
&x - Dereference:
*p - Pointer to Struct:
&Person - Nil Pointer:
nil
// Pointers in Go
package main
import "fmt"
func swap(a, b *int) {
*a, *b = *b, *a
}
func increment(val *int) {
*val++
}
func main() {
x := 10
y := 20
fmt.Printf("Before swap: x=%d, y=%d\n", x, y)
swap(&x, &y)
fmt.Printf("After swap: x=%d, y=%d\n", x, y)
z := 5
fmt.Printf("Before increment: z=%d\n", z)
increment(&z)
fmt.Printf("After increment: z=%d\n", z)
// Pointer to struct
person := &Person{Name: "Alice", Age: 25}
person.Age = 26 // Equivalent to (*person).Age = 26
fmt.Printf("Person: %+v\n", person)
}Goroutines are lightweight threads managed by the Go runtime. They make concurrent programming easy and efficient.
- Start:
go function() - Lightweight: Stack grows dynamically
- Concurrency: Run multiple goroutines
- Communication: Use channels for synchronization
- Main Goroutine: Entry point of program
// Goroutines in Go
package main
import (
"fmt"
"time"
)
func printNumbers(prefix string) {
for i := 1; i <= 5; i++ {
fmt.Printf("%s: %d\n", prefix, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Start goroutines
go printNumbers("Goroutine 1")
go printNumbers("Goroutine 2")
// Give goroutines time to run
time.Sleep(2 * time.Second)
fmt.Println("Main function finished")
}Channels are pipes for communication between goroutines. They provide a way to send and receive values safely.
- Declaration:
ch := make(chan int) - Send:
ch <- value - Receive:
value := <-ch - Buffered Channels:
make(chan int, 10) - Close:
close(ch)
// Channels in Go
package main
import (
"fmt"
"time"
)
func main() {
// Unbuffered channel
ch := make(chan int)
// Send data in goroutine
go func() {
ch <- 42
}()
// Receive data
value := <-ch
fmt.Printf("Received: %d\n", value)
// Buffered channel (capacity 2)
buffered := make(chan int, 2)
buffered <- 1
buffered <- 2
// buffered <- 3 // This would block (buffer full)
fmt.Printf("Buffered channel: %d, %d\n", <-buffered, <-buffered)
// Channel with goroutines
messages := make(chan string)
go func() {
messages <- "Hello"
messages <- "World"
close(messages)
}()
// Range over channel until closed
for msg := range messages {
fmt.Println(msg)
}
// Select statement for multiple channels
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
// Channel directions
func sendOnly(ch chan<- int) {
ch <- 100
}
func receiveOnly(ch <-chan int) {
value := <-ch
fmt.Printf("Received: %d\n", value)
}
ch3 := make(chan int)
go sendOnly(ch3)
receiveOnly(ch3)
}Select allows a goroutine to wait on multiple channel operations. It's similar to switch but for channels.
- Select:
select { case <-ch: } - Non-blocking:
defaultcase - Timeout: Use
time.After - Random Selection: Randomly picks ready case
- Fan-out/Fan-in: Pattern with select
// Select Statement in Go
package main
import (
"fmt"
"time"
)
func main() {
// Basic select
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Message from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Message from ch2"
}()
// Select with timeout
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout!")
}
// Non-blocking select with default
ch3 := make(chan int)
select {
case val := <-ch3:
fmt.Printf("Received: %d\n", val)
default:
fmt.Println("No data available")
}
// Fan-in pattern (multiple channels to one)
fanIn := func(ch1, ch2 <-chan string) <-chan string {
out := make(chan string)
go func() {
for {
select {
case msg := <-ch1:
out <- msg
case msg := <-ch2:
out <- msg
}
}
}()
return out
}
c1 := make(chan string)
c2 := make(chan string)
go func() {
c1 <- "From channel 1"
}()
go func() {
c2 <- "From channel 2"
}()
out := fanIn(c1, c2)
fmt.Println(<-out)
fmt.Println(<-out)
// Fan-out pattern (one channel to multiple)
work := make(chan int)
done := make(chan bool)
// Worker
worker := func(id int, work <-chan int, done chan<- bool) {
for w := range work {
fmt.Printf("Worker %d processing: %d\n", id, w)
time.Sleep(100 * time.Millisecond)
}
done <- true
}
// Send work
go func() {
for i := 1; i <= 5; i++ {
work <- i
}
close(work)
}()
// Start workers
for i := 1; i <= 3; i++ {
go worker(i, work, done)
}
// Wait for all workers
for i := 1; i <= 3; i++ {
<-done
}
fmt.Println("All workers done")
}Go handles errors explicitly using the error interface. Functions return errors as the last return value.
- error Interface:
type error interface { Error() string } - Return Error:
func() (T, error) - Check Error:
if err != nil { return err } - Custom Errors: Implement error interface
- Panic/Recover: For exceptional cases
// Error Handling in Go
package main
import (
"errors"
"fmt"
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
// Function that returns error
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Function with custom error
func validateAge(age int) error {
if age < 0 {
return ValidationError{
Field: "age",
Message: "age cannot be negative",
}
}
if age > 150 {
return ValidationError{
Field: "age",
Message: "age cannot exceed 150",
}
}
return nil
}
// Function with multiple return values including error
func processUser(name string, age int) (string, error) {
if name == "" {
return "", errors.New("name cannot be empty")
}
if err := validateAge(age); err != nil {
return "", err
}
return fmt.Sprintf("User: %s, Age: %d", name, age), nil
}
// Panic and recover example
func riskyOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
}
}()
panic("something went wrong")
}
func main() {
// Basic error handling
result, err := divide(10, 2)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Result: %d\n", result)
}
// Error handling with custom error
if err := validateAge(200); err != nil {
fmt.Printf("Validation error: %v\n", err)
}
// Multiple returns with error
user, err := processUser("Alice", 25)
if err != nil {
fmt.Printf("Error processing user: %v\n", err)
} else {
fmt.Printf("Processed: %s\n", user)
}
// Error checking pattern
data, err := processUser("", 30)
if err != nil {
fmt.Printf("Error: %v\n", err)
// Handle error
return
}
fmt.Printf("Data: %s\n", data)
// Panic and recover
riskyOperation()
fmt.Println("Program continues after panic")
}Defer schedules a function call to run after the surrounding function returns. Panic stops normal execution. Recover regains control.
- Defer:
defer func() - Stack: Defer functions execute in LIFO order
- Panic:
panic("message") - Recover:
recover()inside deferred function - Use Cases: Cleanup, resource management
// Defer, Panic, Recover in Go
package main
import "fmt"
func deferExample() {
defer fmt.Println("Deferred: This runs last")
fmt.Println("Regular: This runs first")
}
func panicExample() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from: %v\n", r)
}
}()
fmt.Println("Before panic")
panic("Something went wrong!")
fmt.Println("After panic") // This won't run
}
func main() {
deferExample()
fmt.Println()
panicExample()
fmt.Println("Program continues after panic recovery")
}Packages are the fundamental building blocks of Go programs. They organize code into reusable units.
- Package Declaration:
package main - Import:
import "fmt" - Exported Names: Uppercase names are exported
- Standard Library: Built-in packages
- Custom Packages: Create your own
// Packages in Go
package main
import (
"fmt"
"math/rand"
"time"
)
// Internal package functions
func add(a, b int) int {
return a + b
}
func main() {
rand.Seed(time.Now().UnixNano())
randomNumber := rand.Intn(100)
fmt.Printf("Random number: %d\n", randomNumber)
fmt.Printf("Add: %d\n", add(10, 20))
}Go provides encoding/json package for JSON serialization and deserialization using struct tags.
- Marshaling:
json.Marshal(v) - Unmarshaling:
json.Unmarshal(data, &v) - Struct Tags:
json:"name" - Omitempty:
json:"name,omitempty" - Indent:
json.MarshalIndent
// JSON in Go
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
}
func main() {
// Marshal to JSON
person := Person{Name: "Alice", Age: 25, Email: "alice@email.com"}
jsonData, err := json.Marshal(person)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("JSON: %s\n", jsonData)
// Pretty print
jsonDataPretty, _ := json.MarshalIndent(person, "", " ")
fmt.Printf("Pretty JSON:\n%s\n", jsonDataPretty)
// Unmarshal from JSON
jsonString := `{"name":"Bob","age":30}`
var person2 Person
err = json.Unmarshal([]byte(jsonString), &person2)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Person2: %+v\n", person2)
}Go provides os and io packages for file operations. Files are opened, read, written, and closed using standard functions.
- Open:
os.Open("file.txt") - Create:
os.Create("file.txt") - Read:
file.Read(buf) - Write:
file.Write(data) - Close:
defer file.Close()
// File I/O in Go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// Write to file
file, err := os.Create("example.txt")
if err != nil {
fmt.Printf("Error creating file: %v\n", err)
return
}
defer file.Close()
_, err = file.WriteString("Hello, World!\n")
if err != nil {
fmt.Printf("Error writing: %v\n", err)
return
}
fmt.Println("File written successfully")
// Read from file
file2, err := os.Open("example.txt")
if err != nil {
fmt.Printf("Error opening file: %v\n", err)
return
}
defer file2.Close()
scanner := bufio.NewScanner(file2)
for scanner.Scan() {
fmt.Printf("Read: %s\n", scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Printf("Error reading: %v\n", err)
}
}Go has a powerful net/http package for building HTTP servers with routing, middleware, and handlers.
- Handler:
http.HandlerFunc - HandleFunc: Register route handlers
- ListenAndServe: Start server
- Request/Response:
*http.Request,http.ResponseWriter - Routing:
http.NewServeMux()
// HTTP Server in Go
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func greetHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "Guest"
}
fmt.Fprintf(w, "Hello, %s!", name)
}
func main() {
http.HandleFunc("/", helloHandler)
http.HandleFunc("/greet", greetHandler)
fmt.Println("Server starting on port 8080...")
http.ListenAndServe(":8080", nil)
}Go has built-in testing support with the testing package. Tests are written in files ending with _test.go.
- Test Functions:
func TestXxx(t *testing.T) - Benchmarks:
func BenchmarkXxx(b *testing.B) - Examples:
func ExampleXxx() - Table-Driven Tests: Test multiple cases
- Run Tests:
go test
// Testing in Go
// math_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
result := add(2, 3)
expected := 5
if result != expected {
t.Errorf("Expected %d, got %d", expected, result)
}
}
func TestSubtract(t *testing.T) {
result := subtract(5, 3)
expected := 2
if result != expected {
t.Errorf("Expected %d, got %d", expected, result)
}
}
// Benchmarks
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
add(2, 3)
}
}
// Example test
func ExampleAdd() {
result := add(2, 3)
fmt.Println(result)
// Output: 5
}Struct Tags are metadata attached to struct fields. They are used for serialization, validation, and documentation.
- JSON Tags:
json:"name" - Validation:
validate:"required" - ORM Tags:
gorm:"column" - Reflection: Access tags via
reflect - Multiple Tags:
json:"name" validate:"required"
// Struct Tags in Go
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Password string `json:"-"`
}
func main() {
user := User{ID: 1, Name: "Alice", Email: "alice@email.com", Password: "secret"}
jsonData, _ := json.Marshal(user)
fmt.Printf("JSON: %s\n", jsonData)
}The empty interface interface can hold values of any type. It's used for generic programming and dynamic typing.
- Any Type:
var v interface - Type Assertion:
v.(type) - Type Switch:
switch v := v.(type) - JSON: Used with
json.Unmarshal - Generic Programming: Before generics
// Empty Interface in Go
package main
import "fmt"
func printValue(v interface{}) {
fmt.Printf("Value: %v, Type: %T\n", v, v)
}
func main() {
printValue(42)
printValue("Hello")
printValue(3.14)
printValue(true)
printValue([]int{1, 2, 3})
}Type Assertion extracts the concrete value from an interface. It can be used with or without a check.
- Basic:
value := interface.(Type) - Safe:
value, ok := interface.(Type) - Type Switch:
switch v := v.(type) - Panic: Fails if type doesn't match
- Use Cases: Unmarshaling, dynamic types
// Type Assertion in Go
package main
import "fmt"
func main() {
var i interface{} = "Hello"
// Type assertion
s, ok := i.(string)
if ok {
fmt.Printf("String: %s\n", s)
}
// Type switch
switch v := i.(type) {
case int:
fmt.Printf("Int: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}Context carries deadlines, cancellation signals, and values across API boundaries. It's essential for managing request-scoped data.
- Background:
context.Background() - WithCancel:
context.WithCancel(ctx) - WithTimeout:
context.WithTimeout(ctx, time.Second) - WithValue:
context.WithValue(ctx, key, val) - Done Channel:
ctx.Done()
// Context in Go
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker stopped")
return
default:
fmt.Println("Working...")
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go worker(ctx)
time.Sleep(2 * time.Second)
fmt.Println("Main function finished")
}The sync package provides synchronization primitives like Mutex, WaitGroup, Once, and RWMutex for concurrent programming.
- Mutex:
sync.Mutex - WaitGroup:
sync.WaitGroup - Once:
sync.Once - RWMutex:
sync.RWMutex - Cond:
sync.Cond
// Sync Package in Go
package main
import (
"fmt"
"sync"
)
var counter int
var mutex sync.Mutex
var wg sync.WaitGroup
func increment() {
mutex.Lock()
counter++
mutex.Unlock()
wg.Done()
}
func main() {
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment()
}
wg.Wait()
fmt.Printf("Counter: %d\n", counter)
}Atomic operations in Go are provided by the sync/atomic package. They ensure safe concurrent access to variables.
- Add:
atomic.AddInt32(&counter, 1) - Load:
atomic.LoadInt32(&counter) - Store:
atomic.StoreInt32(&counter, 0) - Swap:
atomic.SwapInt32(&counter, 10) - CompareAndSwap:
atomic.CompareAndSwapInt32(&counter, old, new)
// Atomic Operations in Go
package main
import (
"fmt"
"sync/atomic"
)
var counter int32
func increment() {
atomic.AddInt32(&counter, 1)
}
func main() {
for i := 0; i < 1000; i++ {
go increment()
}
// Wait for goroutines to finish
// (In real code, use WaitGroup)
fmt.Printf("Counter: %d\n", atomic.LoadInt32(&counter))
}WaitGroup waits for a collection of goroutines to finish. It's used for synchronization and ensuring all tasks complete.
- Add:
wg.Add(1) - Done:
defer wg.Done() - Wait:
wg.Wait() - Counter: Track number of goroutines
- Use Cases: Parallel processing, fan-out
// WaitGroup in Go
package main
import (
"fmt"
"sync"
"time"
)
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")
}Once ensures a function is executed exactly once. It's used for lazy initialization and singletons.
- Do:
once.Do(func() ) - Thread-Safe: Safe for concurrent use
- Singleton: Initialize singleton instance
- Lazy Initialization: Initialize on first use
- Once Example: Database connection
// Once in Go
package main
import (
"fmt"
"sync"
)
var once sync.Once
var instance *Singleton
type Singleton struct {
Data string
}
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{Data: "Initialized"}
fmt.Println("Singleton created")
})
return instance
}
func main() {
for i := 0; i < 5; i++ {
go func() {
inst := GetInstance()
fmt.Printf("Instance: %p\n", inst)
}()
}
// Wait for goroutines
var wg sync.WaitGroup
wg.Add(1)
wg.Wait()
}Timer schedules a single event. Ticker schedules periodic events. Both are used for time-based operations.
- Timer:
time.NewTimer(duration) - Ticker:
time.NewTicker(duration) - Channel:
timer.Candticker.C - Stop:
timer.Stop()andticker.Stop() - After:
time.After(duration)
// Timer and Ticker in Go
package main
import (
"fmt"
"time"
)
func main() {
// Timer
timer := time.NewTimer(2 * time.Second)
<-timer.C
fmt.Println("Timer expired")
// Ticker
ticker := time.NewTicker(500 * time.Millisecond)
go func() {
for t := range ticker.C {
fmt.Printf("Tick at %v\n", t)
}
}()
time.Sleep(2 * time.Second)
ticker.Stop()
fmt.Println("Ticker stopped")
}Mutex (Mutual Exclusion) provides locking mechanisms to protect shared resources from concurrent access.
- Lock:
mutex.Lock() - Unlock:
mutex.Unlock() - Defer Unlock:
defer mutex.Unlock() - Data Race: Prevents race conditions
- Critical Section: Protected code block
// Mutex in Go
package main
import (
"fmt"
"sync"
"time"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := Counter{}
for i := 0; i < 100; i++ {
go counter.Increment()
}
time.Sleep(time.Second)
fmt.Printf("Counter: %d\n", counter.Value())
}RWMutex is a reader/writer mutex that allows multiple readers or one writer. It improves performance for read-heavy workloads.
- RLock:
rwmutex.RLock() - RUnlock:
rwmutex.RUnlock() - Lock:
rwmutex.Lock() - Unlock:
rwmutex.Unlock() - Multiple Readers: Concurrent reads allowed
// RWMutex in Go
package main
import (
"fmt"
"sync"
"time"
)
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}
func (s *SafeMap) Set(key string, value int) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = value
}
func (s *SafeMap) Get(key string) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.data[key]
return val, ok
}
func main() {
sm := SafeMap{data: make(map[string]int)}
for i := 0; i < 10; i++ {
go func(i int) {
sm.Set(fmt.Sprintf("key%d", i), i)
}(i)
}
time.Sleep(time.Second)
for i := 0; i < 10; i++ {
val, ok := sm.Get(fmt.Sprintf("key%d", i))
if ok {
fmt.Printf("key%d: %d\n", i, val)
}
}
}Context With Cancel creates a context that can be cancelled. It's used to propagate cancellation signals across goroutines.
- WithCancel:
context.WithCancel(ctx) - Cancel Function:
cancel() - Done Channel:
ctx.Done() - Propagation: Cancellation propagates to children
- Cleanup: Defer cancel for resource cleanup
// Context With Cancel in Go
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Task cancelled")
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go longRunningTask(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
fmt.Println("Main done")
}Context With Value stores key-value pairs in the context. It's used for request-scoped data like user IDs and request IDs.
- WithValue:
context.WithValue(ctx, key, value) - Value:
ctx.Value(key) - Type Safety: Use custom types for keys
- Use Cases: Authentication, request IDs, tracing
- Limited Scope: Not for optional parameters
// Context With Value in Go
package main
import (
"context"
"fmt"
)
type key string
func main() {
ctx := context.WithValue(context.Background(), key("userID"), "12345")
ctx = context.WithValue(ctx, key("requestID"), "req-abc-123")
processRequest(ctx)
}
func processRequest(ctx context.Context) {
userID := ctx.Value(key("userID"))
requestID := ctx.Value(key("requestID"))
fmt.Printf("UserID: %v, RequestID: %v\n", userID, requestID)
}Custom errors implement the error interface. They can include additional fields and methods for richer error information.
- Error Interface:
type error interface { Error() string } - Struct Error:
type MyError struct { Code int } - Error Method:
func (e MyError) Error() string - Errors Package:
errors.New("message") - Wrapping:
fmt.Errorf("context: %w", err)
// Custom Errors in Go
package main
import (
"errors"
"fmt"
)
// Custom error type 1: Simple struct error
type MyError struct {
Code int
Message string
}
func (e MyError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}
// Custom error type 2: With additional methods
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed for %s: %s (value: %v)",
e.Field, e.Message, e.Value)
}
// Additional method for ValidationError
func (e ValidationError) IsValid() bool {
return false
}
// Custom error type 3: Wrapping errors
type DatabaseError struct {
Err error
Query string
Context string
}
func (e DatabaseError) Error() string {
return fmt.Sprintf("database error in %s: %v", e.Context, e.Err)
}
// Unwrap method for error wrapping
func (e DatabaseError) Unwrap() error {
return e.Err
}
// Function that returns custom error
func validateUser(name string, age int) error {
if name == "" {
return ValidationError{
Field: "name",
Value: name,
Message: "name cannot be empty",
}
}
if age < 0 || age > 150 {
return ValidationError{
Field: "age",
Value: age,
Message: "age must be between 0 and 150",
}
}
return nil
}
// Function with wrapped error
func queryDatabase(query string) error {
// Simulate database error
return DatabaseError{
Err: errors.New("connection refused"),
Query: query,
Context: "queryDatabase",
}
}
func main() {
// Using custom error
err := validateUser("", 25)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
err = validateUser("Alice", 200)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
// Using errors package
err = errors.New("something went wrong")
fmt.Printf("Simple error: %v\n", err)
// Error wrapping
err = fmt.Errorf("context: %w", errors.New("original error"))
fmt.Printf("Wrapped error: %v\n", err)
// Check for specific error type
err = queryDatabase("SELECT * FROM users")
if err != nil {
fmt.Printf("Database error: %v\n", err)
// Unwrap to check underlying error
if unwrapped := errors.Unwrap(err); unwrapped != nil {
fmt.Printf("Underlying error: %v\n", unwrapped)
}
}
// Using errors.Is and errors.As
var dbErr DatabaseError
if errors.As(err, &dbErr) {
fmt.Printf("Database error context: %s\n", dbErr.Context)
}
}Build Tags control which files are included in a build. They enable platform-specific code and conditional compilation.
- Syntax:
// +build linux - Boolean Expressions:
// +build linux,amd64 - Negation:
// +build !windows - Multiple Tags:
// +build linux amd64 - Go Generate:
//go:generate
// Build Tags in Go
// +build linux,amd64
package main
import "fmt"
func main() {
fmt.Println("Linux AMD64 build")
}Embedding allows a struct to include another struct's fields and methods. It's similar to inheritance but uses composition.
- Anonymous Field:
type A struct { B } - Method Promotion: Embedded methods are promoted
- Overriding: Override embedded methods
- Interfaces: Embed interfaces for combination
- Composition: Preferred over inheritance
// Embedding in Go
package main
import "fmt"
// Base struct
type Person struct {
Name string
Age int
}
func (p Person) Greet() string {
return fmt.Sprintf("Hello, I'm %s", p.Name)
}
func (p Person) GetAge() int {
return p.Age
}
// Embedding Person in Employee
type Employee struct {
Person // Anonymous field (embedding)
EmployeeID int
Department string
Salary float64
}
// Override Greet method
func (e Employee) Greet() string {
return fmt.Sprintf("Hello, I'm %s (Employee #%d)", e.Name, e.EmployeeID)
}
// Additional method for Employee
func (e Employee) Work() string {
return fmt.Sprintf("%s is working in %s", e.Name, e.Department)
}
// Embedding with interface
type Reader interface {
Read() string
}
type Writer interface {
Write(data string)
}
// Embedding interfaces
type ReadWriter interface {
Reader
Writer
}
// Struct implementing ReadWriter
type FileHandler struct {
filename string
}
func (f FileHandler) Read() string {
return fmt.Sprintf("Reading from %s", f.filename)
}
func (f FileHandler) Write(data string) {
fmt.Printf("Writing '%s' to %s\n", data, f.filename)
}
// Multiple embedding
type Contact struct {
Email string
Phone string
}
type Address struct {
Street string
City string
Country string
}
type Customer struct {
Person // Embed Person
Contact // Embed Contact
Address // Embed Address
CustomerID int
}
func main() {
// Create Employee with embedded Person
emp := Employee{
Person: Person{
Name: "Alice",
Age: 30,
},
EmployeeID: 1001,
Department: "Engineering",
Salary: 75000.0,
}
// Access embedded fields directly
fmt.Printf("Name: %s\n", emp.Name)
fmt.Printf("Age: %d\n", emp.Age)
// Access embedded methods
fmt.Println(emp.Greet()) // Overridden method
fmt.Printf("Age from embedded: %d\n", emp.GetAge())
fmt.Println(emp.Work())
// Interface embedding
var rw ReadWriter = FileHandler{filename: "data.txt"}
fmt.Println(rw.Read())
rw.Write("Hello World")
// Multiple embedding
customer := Customer{
Person: Person{
Name: "Bob",
Age: 25,
},
Contact: Contact{
Email: "bob@email.com",
Phone: "123-456-7890",
},
Address: Address{
Street: "123 Main St",
City: "New York",
Country: "USA",
},
CustomerID: 2001,
}
// Access fields from all embedded structs
fmt.Printf("Customer: %s, Age: %d, Email: %s, City: %s\n",
customer.Name, customer.Age, customer.Email, customer.City)
// Overriding embedded fields
type Manager struct {
Employee
TeamSize int
}
manager := Manager{
Employee: Employee{
Person: Person{
Name: "Charlie",
Age: 35,
},
EmployeeID: 1002,
Department: "Management",
Salary: 100000.0,
},
TeamSize: 10,
}
fmt.Printf("Manager: %s manages %d people\n", manager.Name, manager.TeamSize)
}Generics were introduced in Go 1.18. They allow writing type-parameterized functions and types for reusable code.
- Type Parameters:
func F[T any](t T) T - Constraints:
func F[T int | float64] - Generic Types:
type Stack[T any] struct - Methods: Methods can use type parameters
- Comparable:
comparableconstraint
// Generics in Go (1.18+)
package main
import "fmt"
// Generic function
func Sum[T int | float64](a, b T) T {
return a + b
}
// Generic struct
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func main() {
fmt.Printf("Sum int: %d\n", Sum[int](5, 3))
fmt.Printf("Sum float: %.2f\n", Sum[float64](3.14, 2.5))
intStack := Stack[int]{}
intStack.Push(10)
intStack.Push(20)
val, _ := intStack.Pop()
fmt.Printf("Stack pop: %d\n", val)
}Reflection allows inspecting and manipulating values at runtime. It's provided by the reflect package.
- Type:
reflect.TypeOf(v) - Value:
reflect.ValueOf(v) - Field Access:
v.FieldByName("Name") - Method Call:
v.MethodByName("Method").Call() - Performance: Reflection is slower
// Reflection in Go
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func inspectStruct(v interface{}) {
t := reflect.TypeOf(v)
fmt.Printf("Type: %s\n", t.Name())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf(" Field %d: %s (%s)\n", i, field.Name, field.Type)
}
}
func main() {
user := User{Name: "Alice", Age: 25}
inspectStruct(user)
// Dynamic field access
v := reflect.ValueOf(user)
nameField := v.FieldByName("Name")
if nameField.IsValid() {
fmt.Printf("Name: %s\n", nameField.String())
}
}iota is a predeclared identifier for constant declarations. It provides a way to create enumerated constants.
- Declaration:
const ( A = iota; B; C ) - Increment: Values increment by 1
- Skip Values:
const ( _ = iota; A; B ) - Bitmask:
1 << iota - Reset: iota resets at each const block
// iota in Go
package main
import "fmt"
// Basic iota usage
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// iota with expressions
const (
_ = iota // 0 (skipped)
KB = 1 << (10 * iota) // 1 << (10*1) = 1024
MB // 1 << (10*2) = 1048576
GB // 1 << (10*3) = 1073741824
TB // 1 << (10*4) = 1099511627776
)
// iota with bitmask
const (
Read = 1 << iota // 1
Write // 2
Execute // 4
// ReadWrite = Read | Write
)
// iota with type
type Priority int
const (
Low Priority = iota // 0
Medium // 1
High // 2
Critical // 3
)
// iota reset in new const block
const (
First = iota // 0
Second // 1
)
const (
Again = iota // 0 (resets)
Again2 // 1
)
// iota with custom calculation
const (
Start = iota * 10 // 0
// 10
// 20
// 30
)
func main() {
// Basic iota
fmt.Printf("Sunday: %d, Monday: %d, Tuesday: %d\n", Sunday, Monday, Tuesday)
// iota with bit shifting
fmt.Printf("KB: %d, MB: %d, GB: %d, TB: %d\n", KB, MB, GB, TB)
// iota as bitmask
permissions := Read | Write
fmt.Printf("Permissions: %d\n", permissions)
fmt.Printf("Has Read: %t, Has Write: %t, Has Execute: %t\n",
permissions&Read != 0, permissions&Write != 0, permissions&Execute != 0)
// iota with custom type
var p Priority = High
fmt.Printf("Priority: %d\n", p)
// iota reset
fmt.Printf("First: %d, Second: %d\n", First, Second)
fmt.Printf("Again: %d, Again2: %d\n", Again, Again2)
// iota with custom calculation
// These values would be: 0, 10, 20, 30
// Uncomment to see usage
}The flag package provides command-line flag parsing. It supports various flag types and custom usage messages.
- String Flag:
flag.String("name", "default", "help") - Int Flag:
flag.Int("age", 0, "help") - Bool Flag:
flag.Bool("verbose", false, "help") - Parse:
flag.Parse() - Args:
flag.Args()
// Flag Package in Go
package main
import (
"flag"
"fmt"
)
func main() {
var name string
var age int
var active bool
flag.StringVar(&name, "name", "Guest", "user name")
flag.IntVar(&age, "age", 0, "user age")
flag.BoolVar(&active, "active", false, "is user active")
flag.Parse()
fmt.Printf("Name: %s\n", name)
fmt.Printf("Age: %d\n", age)
fmt.Printf("Active: %t\n", active)
fmt.Printf("Args: %v\n", flag.Args())
}Go has a built-in log package for logging. It supports log levels, prefixes, flags, and output destinations.
- Basic Log:
log.Println("message") - Prefix:
log.SetPrefix("ERROR: ") - Flags:
log.SetFlags(log.LstdFlags) - File Output:
log.SetOutput(file) - Fatal:
log.Fatal("message")
// Logging in Go
package main
import (
"log"
"os"
)
func main() {
// Basic log
log.Println("This is a log message")
// Log with prefix
log.SetPrefix("ERROR: ")
log.Println("This is an error message")
// Log with flags
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("Log with file and line")
// Log to file
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
defer file.Close()
log.SetOutput(file)
log.Println("This will go to the log file")
}Environment variables in Go are accessed using the os package. They provide configuration and runtime settings.
- Get:
os.Getenv("KEY") - Set:
os.Setenv("KEY", "value") - Lookup:
os.LookupEnv("KEY") - Environ:
os.Environ() - Unset:
os.Unsetenv("KEY")
// Environment Variables in Go
package main
import (
"fmt"
"os"
)
func main() {
// Get environment variable
home := os.Getenv("HOME")
fmt.Printf("HOME: %s\n", home)
// Set environment variable
os.Setenv("MY_VAR", "Hello World")
fmt.Printf("MY_VAR: %s\n", os.Getenv("MY_VAR"))
// Get all environment variables
env := os.Environ()
for _, e := range env[:5] {
fmt.Println(e)
}
// Lookup environment variable
val, exists := os.LookupEnv("PATH")
if exists {
fmt.Printf("PATH exists: %s\n", val)
}
}Command line arguments in Go are accessed through os.Args. The flag package provides more advanced parsing.
- Args:
os.Args - Index 0: Program name
- Count:
len(os.Args) - Flag Package:
flag.Parse() - Positional Args:
flag.Args()
// Command Line Arguments in Go
package main
import (
"fmt"
"os"
)
func main() {
// Get arguments
args := os.Args
fmt.Printf("Number of arguments: %d\n", len(args))
for i, arg := range args {
fmt.Printf("Argument %d: %s\n", i, arg)
}
// Check arguments
if len(args) > 1 {
fmt.Printf("First argument: %s\n", args[1])
}
}Signal handling in Go uses the os/signal package. It allows programs to respond to system signals for graceful shutdown.
- Notify:
signal.Notify(ch, syscall.SIGINT) - Signals: SIGINT, SIGTERM, SIGHUP
- Channel:
make(chan os.Signal, 1) - Block:
<-chto wait for signal - Graceful Shutdown: Clean up resources
// OS Signal Handling in Go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Create a channel to receive signals
sigChan := make(chan os.Signal, 1)
// Notify the channel for specific signals
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
// Create a channel to signal when to stop
done := make(chan bool)
// Start a goroutine that does work
go func() {
fmt.Println("Application started. Press Ctrl+C to stop...")
for {
select {
case <-done:
fmt.Println("Goroutine stopping...")
return
default:
fmt.Println("Working...")
time.Sleep(2 * time.Second)
}
}
}()
// Block until a signal is received
sig := <-sigChan
fmt.Printf("Received signal: %s\n", sig)
// Graceful shutdown
fmt.Println("Starting graceful shutdown...")
// Signal the goroutine to stop
close(done)
// Wait for cleanup
time.Sleep(1 * time.Second)
fmt.Println("Application stopped gracefully")
}
// Example with multiple signal handling
func signalHandlerExample() {
// Create a channel for signals
sigs := make(chan os.Signal, 1)
// Notify for multiple signals
signal.Notify(sigs,
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM, // Termination
syscall.SIGHUP, // Hangup
syscall.SIGUSR1, // User signal 1
syscall.SIGUSR2, // User signal 2
)
// Handle signals in a goroutine
go func() {
for sig := range sigs {
switch sig {
case syscall.SIGINT:
fmt.Println("Received SIGINT - Interrupt")
case syscall.SIGTERM:
fmt.Println("Received SIGTERM - Terminate")
case syscall.SIGHUP:
fmt.Println("Received SIGHUP - Hangup")
case syscall.SIGUSR1:
fmt.Println("Received SIGUSR1 - User signal 1")
case syscall.SIGUSR2:
fmt.Println("Received SIGUSR2 - User signal 2")
}
}
}()
}
// Graceful shutdown with context
func gracefulShutdownExample() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
// Simulate server
server := struct {
running bool
}{running: true}
go func() {
for server.running {
fmt.Println("Server is running...")
time.Sleep(1 * time.Second)
}
}()
// Wait for signal
<-sigs
fmt.Println("Shutting down server...")
// Cleanup
server.running = false
fmt.Println("Server stopped")
}Go's http.Client provides a powerful HTTP client with support for GET, POST, headers, and timeouts.
- Client:
http.Client{Timeout: 10 * time.Second} - GET:
http.Get(url) - POST:
http.Post(url, contentType, body) - Headers:
req.Header.Set("key", "value") - Response:
resp.Bodyandresp.StatusCode
// HTTP Client in Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
func main() {
// Basic HTTP client
client := &http.Client{
Timeout: 10 * time.Second,
}
// GET request
resp, err := client.Get("https://jsonplaceholder.typicode.com/posts/1")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
// Read response body
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading body: %v\n", err)
return
}
fmt.Printf("Status: %d\n", resp.StatusCode)
fmt.Printf("Body: %s\n", string(body))
// POST request with JSON
type Post struct {
Title string `json:"title"`
Body string `json:"body"`
UserID int `json:"userId"`
}
postData := Post{
Title: "Hello World",
Body: "This is a test post",
UserID: 1,
}
jsonData, err := json.Marshal(postData)
if err != nil {
fmt.Printf("Error marshaling JSON: %v\n", err)
return
}
// POST request
resp, err = client.Post(
"https://jsonplaceholder.typicode.com/posts",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
body, _ = io.ReadAll(resp.Body)
fmt.Printf("POST Status: %d\n", resp.StatusCode)
fmt.Printf("POST Response: %s\n", string(body))
// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
return
}
// Set headers
req.Header.Set("Authorization", "Bearer your-token")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "MyApp/1.0")
// Execute request
resp, err = client.Do(req)
if err != nil {
fmt.Printf("Error executing request: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Custom request status: %d\n", resp.StatusCode)
// Client with custom transport
transport := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: nil,
}
customClient := &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
}
// Use custom client
resp, err = customClient.Get("https://example.com")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Custom client status: %d\n", resp.StatusCode)
}Setting a timeout on the HTTP client prevents hanging requests and improves reliability.
- Client Timeout:
http.Client{Timeout: 5 * time.Second} - Context Timeout:
context.WithTimeout - Transport Timeout:
http.Transport{ResponseHeaderTimeout: 2 * time.Second} - Deadline:
req.WithContext(ctx) - Cancel:
cancel()function
// HTTP Client with Timeout in Go
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
// Method 1: Client-level timeout
client := &http.Client{
Timeout: 5 * time.Second, // Total timeout for the request
}
// Simple GET with client timeout
resp, err := client.Get("https://httpbin.org/delay/10") // This will timeout
if err != nil {
fmt.Printf("Client timeout error: %v\n", err)
} else {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Response: %s\n", string(body))
}
// Method 2: Context timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/5", nil)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
return
}
client2 := &http.Client{}
resp, err = client2.Do(req)
if err != nil {
fmt.Printf("Context timeout error: %v\n", err)
} else {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Response: %s\n", string(body))
}
// Method 3: Transport-level timeout
transport := &http.Transport{
ResponseHeaderTimeout: 2 * time.Second, // Timeout for response headers
TLSHandshakeTimeout: 3 * time.Second,
IdleConnTimeout: 30 * time.Second,
}
client3 := &http.Client{
Transport: transport,
Timeout: 10 * time.Second, // Overall timeout
}
resp, err = client3.Get("https://httpbin.org/delay/8")
if err != nil {
fmt.Printf("Transport timeout error: %v\n", err)
} else {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Response: %s\n", string(body))
}
// Method 4: Combined timeouts with deadline
deadlineCtx, deadlineCancel := context.WithDeadline(
context.Background(),
time.Now().Add(4*time.Second),
)
defer deadlineCancel()
req2, err := http.NewRequestWithContext(deadlineCtx, "GET", "https://httpbin.org/delay/3", nil)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
return
}
client4 := &http.Client{}
resp, err = client4.Do(req2)
if err != nil {
fmt.Printf("Deadline timeout error: %v\n", err)
} else {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Response: %s\n", string(body))
}
// Method 5: Custom timeout with cancel
customCtx, customCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer customCancel()
// Start the request
req3, err := http.NewRequestWithContext(customCtx, "GET", "https://httpbin.org/delay/10", nil)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
return
}
client5 := &http.Client{}
resp, err = client5.Do(req3)
if err != nil {
fmt.Printf("Request cancelled: %v\n", err)
} else {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Response: %s\n", string(body))
}
// Example: Checking if error is timeout
req4, _ := http.NewRequest("GET", "https://httpbin.org/delay/10", nil)
ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel2()
req4 = req4.WithContext(ctx2)
resp, err = client.Do(req4)
if err != nil {
if ctx2.Err() == context.DeadlineExceeded {
fmt.Println("Request timed out due to context deadline")
} else {
fmt.Printf("Other error: %v\n", err)
}
} else {
defer resp.Body.Close()
fmt.Println("Request completed successfully")
}
}Custom HTTP clients allow fine-grained control over transport, timeouts, headers, and connection pooling.
- Transport:
&http.Transport{MaxIdleConns: 10} - Connection Pooling: Reuse connections
- Timeouts: Connect, read, write timeouts
- Headers: Set default headers
- Redirect:
CheckRedirectpolicy
// Custom HTTP Client in Go
package main
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"time"
)
func main() {
// Custom Transport with connection pooling
transport := &http.Transport{
// Connection pooling
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
// Connection settings
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
// TLS settings
TLSClientConfig: &tls.Config{
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
},
// Response header timeout
ResponseHeaderTimeout: 5 * time.Second,
// Expect continue timeout
ExpectContinueTimeout: 1 * time.Second,
}
// Custom Client
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
// Custom redirect policy
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
}
return nil
},
}
// Add default headers using RoundTrip wrapper
type customTransport struct {
transport http.RoundTripper
headers map[string]string
}
func (c customTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Add default headers
for key, value := range c.headers {
req.Header.Set(key, value)
}
return c.transport.RoundTrip(req)
}
// Create client with default headers
headerTransport := customTransport{
transport: transport,
headers: map[string]string{
"User-Agent": "MyCustomApp/1.0",
"Accept": "application/json",
"Accept-Language": "en-US,en;q=0.9",
},
}
clientWithHeaders := &http.Client{
Transport: headerTransport,
Timeout: 30 * time.Second,
}
// Use custom client
resp, err := clientWithHeaders.Get("https://httpbin.org/headers")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Status: %d\n", resp.StatusCode)
// Custom client with proxy
proxyURL, _ := url.Parse("http://proxy.example.com:8080")
proxyTransport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
MaxIdleConns: 100,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
proxyClient := &http.Client{
Transport: proxyTransport,
Timeout: 30 * time.Second,
}
// Use proxy client
resp, err = proxyClient.Get("https://api.example.com/data")
if err != nil {
fmt.Printf("Proxy error: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Proxy client status: %d\n", resp.StatusCode)
// Client with retry capability (custom wrapper)
retryClient := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
// Custom function to retry requests
doRequest := func(req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i < 3; i++ {
resp, err = retryClient.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
time.Sleep(time.Duration(i+1) * time.Second)
}
return resp, err
}
req, _ := http.NewRequest("GET", "https://httpbin.org/status/500", nil)
resp, err = doRequest(req)
if err != nil {
fmt.Printf("Request failed after retries: %v\n", err)
} else {
defer resp.Body.Close()
fmt.Printf("Retry client status: %d\n", resp.StatusCode)
}
}Go's http.ServeMux provides routing for HTTP requests. It can handle different methods and URL patterns.
- ServeMux:
http.NewServeMux() - HandleFunc: Register handlers
- Method Handling: Check
r.Method - Path Parameters: Extract from URL
- Static Files:
http.FileServer
// HTTP Server with Routing in Go
package main
import (
"fmt"
"net/http"
)
func main() {
// Serve static files
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
// API routes
http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
fmt.Fprintf(w, "GET users")
case "POST":
fmt.Fprintf(w, "POST users")
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
http.HandleFunc("/api/users/", func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Path[len("/api/users/"):]
fmt.Fprintf(w, "User ID: %s\n", id)
})
fmt.Println("Server starting on port 8080...")
http.ListenAndServe(":8080", nil)
}Middleware are functions that wrap HTTP handlers to provide cross-cutting concerns like logging, auth, and CORS.
- Handler:
http.Handlerinterface - Chain: Compose multiple middleware
- Logging: Log requests and responses
- Auth: Authentication middleware
- Recovery: Panic recovery middleware
// Middleware in Go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("Started %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("Completed in %v", 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
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
// Apply middleware
handler := loggingMiddleware(authMiddleware(mux))
http.ListenAndServe(":8080", handler)
}WebSocket provides full-duplex communication channels over a single TCP connection. Go supports WebSockets via packages like gorilla/websocket.
- Upgrader:
websocket.Upgrader - Read:
conn.ReadMessage() - Write:
conn.WriteMessage() - Handshake: Upgrade HTTP to WebSocket
- Close:
conn.Close()
// WebSocket in Go
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("Upgrade failed:", err)
return
}
defer conn.Close()
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Read failed:", err)
break
}
fmt.Printf("Received: %s\n", message)
err = conn.WriteMessage(messageType, message)
if err != nil {
log.Println("Write failed:", err)
break
}
}
}
func main() {
http.HandleFunc("/ws", handleWebSocket)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}gRPC is a high-performance RPC framework. Go supports gRPC with protocol buffers for efficient communication.
- Service Definition: Protocol buffers
- Server:
grpc.NewServer() - Register:
pb.RegisterGreeterServer(s, &server) - Listen:
net.Listen("tcp", ":50051") - Serve:
s.Serve(lis)
// gRPC Server in Go
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/proto"
)
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + req.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
log.Println("Server listening on :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}The gRPC client connects to a gRPC server and calls remote methods with proper context and error handling.
- Connection:
grpc.Dial("localhost:50051", grpc.WithInsecure()) - Client:
pb.NewGreeterClient(conn) - Call:
client.SayHello(ctx, &pb.HelloRequest) - Context:
context.WithTimeout - Error: Handle errors from RPC calls
// gRPC Client in Go
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
pb "path/to/proto"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: "World"})
if err != nil {
log.Fatalf("Could not greet: %v", err)
}
log.Printf("Greeting: %s", resp.Message)
}Go's database/sql package provides a generic interface for database connections with various drivers.
- Open:
sql.Open("driver", "connection string") - Ping:
db.Ping() - Query:
db.Query("SELECT * FROM users") - Exec:
db.Exec("INSERT INTO users VALUES (?)", name) - Rows: Scan results with
rows.Scan()
// Database Connection in Go
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// Connect to database
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer db.Close()
// Test connection
err = db.Ping()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Connected to database")
// Query
rows, err := db.Query("SELECT id, name, age FROM users")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer rows.Close()
for rows.Next() {
var id int
var name string
var age int
err = rows.Scan(&id, &name, &age)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("ID: %d, Name: %s, Age: %d\n", id, name, age)
}
}GORM is a popular ORM for Go. It provides automatic mapping between structs and database tables.
- Model: Struct with gorm tags
- Create:
db.Create(&user) - Read:
db.First(&user, 1) - Update:
db.Model(&user).Update("Age", 26) - Delete:
db.Delete(&user)
// ORM in Go with GORM
package main
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string
Email string
Age int
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Auto migrate
db.AutoMigrate(&User{})
// Create
db.Create(&User{Name: "Alice", Email: "alice@email.com", Age: 25})
// Read
var user User
db.First(&user, 1)
fmt.Printf("User: %+v\n", user)
// Update
db.Model(&user).Update("Age", 26)
// Delete
db.Delete(&user)
}MongoDB in Go uses the official mongo driver. It provides a fluent API for CRUD operations.
- Connect:
mongo.Connect(ctx, options.Client()) - Database:
client.Database("dbname") - Collection:
db.Collection("users") - Insert:
collection.InsertOne(ctx, doc) - Find:
collection.Find(ctx, filter)
// MongoDB in Go
package main
import (
"context"
"fmt"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type User struct {
Name string
Email string
Age int
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer client.Disconnect(ctx)
collection := client.Database("test").Collection("users")
// Insert
user := User{Name: "Alice", Email: "alice@email.com", Age: 25}
result, err := collection.InsertOne(ctx, user)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Inserted ID: %v\n", result.InsertedID)
}Redis in Go uses the go-redis library. It provides a client for Redis operations with context support.
- Client:
redis.NewClient(&redis.Options) - Set:
rdb.Set(ctx, "key", "value", 0) - Get:
rdb.Get(ctx, "key").Result() - Incr:
rdb.Incr(ctx, "counter") - Del:
rdb.Del(ctx, "key")
// Redis in Go
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
// Set
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Get
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Value: %s\n", val)
// Incr
rdb.Incr(ctx, "counter")
rdb.Incr(ctx, "counter")
counter, _ := rdb.Get(ctx, "counter").Int()
fmt.Printf("Counter: %d\n", counter)
}Testify is a testing toolkit for Go that provides assertions, mocking, and suite functionality.
- Assert:
assert.Equal(t, expected, actual) - Require:
require.NoError(t, err) - Mock:
mock.On("Method", args).Return(values) - Suite:
suite.Run(t, new(MySuite)) - Before/After: Setup and teardown
// Testing with Testify in Go
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdd(t *testing.T) {
result := add(2, 3)
assert.Equal(t, 5, result, "Add should return sum")
result2 := add(-1, 1)
assert.Equal(t, 0, result2)
}
func TestDivide(t *testing.T) {
result, err := divide(10, 2)
require.NoError(t, err)
assert.Equal(t, 5, result)
_, err = divide(10, 0)
assert.Error(t, err)
}
func TestMain(m *testing.M) {
// Setup
println("Setup")
code := m.Run()
// Teardown
println("Teardown")
os.Exit(code)
}Benchmarks measure the performance of code. They are written in test files and run with the go test -bench command.
- Function:
func BenchmarkXxx(b *testing.B) - Loop:
for i := 0; i < b.N; i++ - Reset:
b.ResetTimer() - Stop:
b.StopTimer()andb.StartTimer() - Report:
go test -bench=. -benchmem
// Benchmarks in Go
package main
import (
"fmt"
"testing"
)
// Function to benchmark
func Sum(numbers []int) int {
sum := 0
for _, n := range numbers {
sum += n
}
return sum
}
// Function to benchmark with allocation
func SumWithAllocation(numbers []int) int {
result := make([]int, len(numbers))
for i, n := range numbers {
result[i] = n
}
sum := 0
for _, n := range result {
sum += n
}
return sum
}
// Benchmark functions (these would be in a _test.go file)
// BenchmarkSum measures Sum function performance
func BenchmarkSum(b *testing.B) {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// Reset timer to exclude setup time
b.ResetTimer()
// Run the benchmark b.N times
for i := 0; i < b.N; i++ {
Sum(numbers)
}
}
// BenchmarkSumWithAllocation measures allocation performance
func BenchmarkSumWithAllocation(b *testing.B) {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
b.ResetTimer()
for i := 0; i < b.N; i++ {
SumWithAllocation(numbers)
}
}
// Benchmark with parallel execution
func BenchmarkSumParallel(b *testing.B) {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Sum(numbers)
}
})
}
// Benchmark with custom timer control
func BenchmarkCustomTimer(b *testing.B) {
// Setup
data := make([]int, 1000)
for i := range data {
data[i] = i
}
// Stop timer for setup
b.StopTimer()
// Expensive setup
processed := make([]int, len(data))
copy(processed, data)
// Start timer for actual test
b.StartTimer()
for i := 0; i < b.N; i++ {
Sum(processed)
}
}
// Example of running benchmarks programmatically
func main() {
// This shows how benchmarks would be run
fmt.Println("To run benchmarks, use:")
fmt.Println("go test -bench=.")
fmt.Println("go test -bench=. -benchmem")
fmt.Println("go test -bench=Sum -benchmem -count=5")
fmt.Println("go test -bench=. -benchtime=10s")
fmt.Println("go test -bench=. -benchtime=100x")
}
// Example test file would contain:
// package main
//
// import "testing"
//
// func TestSum(t *testing.T) {
// numbers := []int{1, 2, 3}
// result := Sum(numbers)
// expected := 6
// if result != expected {
// t.Errorf("Sum(%v) = %d; want %d", numbers, result, expected)
// }
// }
//
// func BenchmarkSum(b *testing.B) {
// numbers := []int{1, 2, 3, 4, 5}
// for i := 0; i < b.N; i++ {
// Sum(numbers)
// }
// }Dependency Injection is a design pattern where dependencies are provided to objects rather than created internally. It improves testability and flexibility.
- Constructor Injection: Pass dependencies via constructor
- Interface Injection: Depend on interfaces
- Function Injection: Pass dependencies to functions
- Container: Dependency injection container
- Testability: Easy to mock dependencies
// Dependency Injection in Go
package main
import "fmt"
// Logger interface
type Logger interface {
Log(message string)
}
// ConsoleLogger implementation
type ConsoleLogger struct{}
func (l ConsoleLogger) Log(message string) {
fmt.Printf("[LOG] %s\n", message)
}
// Service with dependency injection
type UserService struct {
logger Logger
}
func NewUserService(logger Logger) *UserService {
return &UserService{logger: logger}
}
func (s *UserService) CreateUser(name string) {
s.logger.Log(fmt.Sprintf("Creating user: %s", name))
fmt.Printf("User created: %s\n", name)
}
func main() {
logger := ConsoleLogger{}
service := NewUserService(logger)
service.CreateUser("Alice")
}Using interfaces for dependency injection decouples implementations and enables easy swapping of dependencies.
- Interface: Define behavior
- Implementation: Implement interface
- Mock: Create mock implementations for testing
- Swap: Change implementations easily
- Testability: Isolate components for testing
// Dependency Injection with Interface in Go
package main
import "fmt"
// Database interface
type Database interface {
Save(data string) error
Get(id int) (string, error)
}
// Mock Database for testing
type MockDB struct{}
func (m MockDB) Save(data string) error {
fmt.Printf("Mock saving: %s\n", data)
return nil
}
func (m MockDB) Get(id int) (string, error) {
return fmt.Sprintf("Mock data for ID: %d", id), nil
}
// Real Database
type RealDB struct{}
func (r RealDB) Save(data string) error {
fmt.Printf("Real saving: %s\n", data)
return nil
}
func (r RealDB) Get(id int) (string, error) {
return fmt.Sprintf("Real data for ID: %d", id), nil
}
type Repository struct {
db Database
}
func NewRepository(db Database) *Repository {
return &Repository{db: db}
}
func main() {
// Use mock for testing
mockRepo := NewRepository(MockDB{})
mockRepo.db.Save("test data")
// Use real for production
realRepo := NewRepository(RealDB{})
realRepo.db.Save("real data")
}The Functional Options Pattern provides a flexible way to configure structs using functions that modify settings.
- Options: Functions that modify config
- Defaults: Set default values
- Flexibility: Configure only what's needed
- Extensibility: Easy to add new options
- Readability: Clear configuration code
// Functional Options Pattern in Go
package main
import "fmt"
type Server struct {
host string
port int
timeout int
maxConns int
}
type ServerOption func(*Server)
func WithHost(host string) ServerOption {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout int) ServerOption {
return func(s *Server) {
s.timeout = timeout
}
}
func WithMaxConns(maxConns int) ServerOption {
return func(s *Server) {
s.maxConns = maxConns
}
}
func NewServer(opts ...ServerOption) *Server {
// Default values
s := &Server{
host: "localhost",
port: 8080,
timeout: 30,
maxConns: 100,
}
// Apply options
for _, opt := range opts {
opt(s)
}
return s
}
func main() {
server1 := NewServer()
fmt.Printf("Server1: %+v\n", server1)
server2 := NewServer(
WithHost("0.0.0.0"),
WithPort(9090),
WithTimeout(60),
WithMaxConns(200),
)
fmt.Printf("Server2: %+v\n", server2)
}The Builder Pattern constructs complex objects step by step. It provides a fluent interface for object creation.
- Builder: Constructs the object
- Fluent Interface: Method chaining
- Immutable: Builds immutable objects
- Flexibility: Optional fields
- Readability: Clear construction code
// Builder Pattern in Go
package main
import "fmt"
type Person struct {
Name string
Age int
Email string
Address string
Phone string
}
type PersonBuilder struct {
person Person
}
func NewPersonBuilder() *PersonBuilder {
return &PersonBuilder{person: Person{}}
}
func (b *PersonBuilder) WithName(name string) *PersonBuilder {
b.person.Name = name
return b
}
func (b *PersonBuilder) WithAge(age int) *PersonBuilder {
b.person.Age = age
return b
}
func (b *PersonBuilder) WithEmail(email string) *PersonBuilder {
b.person.Email = email
return b
}
func (b *PersonBuilder) WithAddress(address string) *PersonBuilder {
b.person.Address = address
return b
}
func (b *PersonBuilder) WithPhone(phone string) *PersonBuilder {
b.person.Phone = phone
return b
}
func (b *PersonBuilder) Build() Person {
return b.person
}
func main() {
person := NewPersonBuilder().
WithName("Alice").
WithAge(25).
WithEmail("alice@email.com").
WithAddress("123 Main St").
WithPhone("555-1234").
Build()
fmt.Printf("Person: %+v\n", person)
}The Singleton Pattern ensures a type has only one instance. In Go, it's implemented using sync.Once for thread safety.
- sync.Once: Ensures single initialization
- Package Level: Instance at package level
- Lazy Loading: Initialize on first use
- Thread Safety: Safe for concurrent use
- Global Access: GetInstance function
// Singleton Pattern in Go
package main
import (
"fmt"
"sync"
)
type Database struct {
connected bool
}
var (
instance *Database
once sync.Once
)
func GetDatabase() *Database {
once.Do(func() {
fmt.Println("Creating database connection...")
instance = &Database{connected: true}
})
return instance
}
func (db *Database) Query(sql string) {
fmt.Printf("Executing query: %s\n", sql)
}
func main() {
db1 := GetDatabase()
db2 := GetDatabase()
db1.Query("SELECT * FROM users")
db2.Query("SELECT * FROM orders")
fmt.Printf("db1 == db2: %v\n", db1 == db2)
fmt.Printf("db1 address: %p\n", db1)
fmt.Printf("db2 address: %p\n", db2)
}The Factory Pattern creates objects without exposing the creation logic. It returns interface types for abstraction.
- Factory Function: Creates objects
- Interface: Returns interface types
- Polymorphism: Creates different types
- Decoupling: Separates creation from usage
- Testability: Easy to mock
// Factory Pattern in Go
package main
import "fmt"
// Product interface
type Product interface {
Use() string
}
// Concrete products
type Chair struct{}
func (c Chair) Use() string {
return "Sitting on a chair"
}
type Table struct{}
func (t Table) Use() string {
return "Placing items on a table"
}
type Sofa struct{}
func (s Sofa) Use() string {
return "Sitting on a sofa"
}
// Factory
type FurnitureFactory struct{}
func (f FurnitureFactory) CreateProduct(productType string) (Product, error) {
switch productType {
case "chair":
return Chair{}, nil
case "table":
return Table{}, nil
case "sofa":
return Sofa{}, nil
default:
return nil, fmt.Errorf("unknown product type: %s", productType)
}
}
func main() {
factory := FurnitureFactory{}
products := []string{"chair", "table", "sofa"}
for _, p := range products {
product, err := factory.CreateProduct(p)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("%s: %s\n", p, product.Use())
}
}The Strategy Pattern defines a family of algorithms and makes them interchangeable. It uses interfaces for algorithm selection.
- Strategy Interface: Defines algorithm
- Concrete Strategies: Implement algorithms
- Context: Uses strategies
- Runtime Selection: Change strategy at runtime
- Flexibility: Easy to add new strategies
// Strategy Pattern in Go
package main
import "fmt"
// Strategy interface
type SortStrategy interface {
Sort(data []int) []int
}
// Bubble sort
type BubbleSort struct{}
func (b BubbleSort) Sort(data []int) []int {
result := make([]int, len(data))
copy(result, data)
n := len(result)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if result[j] > result[j+1] {
result[j], result[j+1] = result[j+1], result[j]
}
}
}
return result
}
// Quick sort
type QuickSort struct{}
func (q QuickSort) Sort(data []int) []int {
result := make([]int, len(data))
copy(result, data)
quickSortHelper(result, 0, len(result)-1)
return result
}
func quickSortHelper(arr []int, low, high int) {
if low < high {
pi := partition(arr, low, high)
quickSortHelper(arr, low, pi-1)
quickSortHelper(arr, pi+1, high)
}
}
func partition(arr []int, low, high int) int {
pivot := arr[high]
i := low - 1
for j := low; j < high; j++ {
if arr[j] < pivot {
i++
arr[i], arr[j] = arr[j], arr[i]
}
}
arr[i+1], arr[high] = arr[high], arr[i+1]
return i + 1
}
// Context
type Sorter struct {
strategy SortStrategy
}
func (s *Sorter) SetStrategy(strategy SortStrategy) {
s.strategy = strategy
}
func (s *Sorter) Sort(data []int) []int {
return s.strategy.Sort(data)
}
func main() {
data := []int{64, 34, 25, 12, 22, 11, 90}
sorter := Sorter{}
sorter.SetStrategy(BubbleSort{})
result1 := sorter.Sort(data)
fmt.Printf("Bubble Sort: %v\n", result1)
sorter.SetStrategy(QuickSort{})
result2 := sorter.Sort(data)
fmt.Printf("Quick Sort: %v\n", result2)
}The Observer Pattern defines a one-to-many dependency where subjects notify observers of state changes.
- Subject: Maintains observers
- Observer: Receives updates
- Register: Subscribe to updates
- Notify: Broadcast changes
- Event-Driven: Reactive programming
// Observer Pattern in Go
package main
import "fmt"
// Observer interface
type Observer interface {
Update(message string)
}
// Subject interface
type Subject interface {
Register(observer Observer)
Unregister(observer Observer)
Notify(message string)
}
// Concrete Subject
type NewsPublisher struct {
observers []Observer
}
func (n *NewsPublisher) Register(observer Observer) {
n.observers = append(n.observers, observer)
}
func (n *NewsPublisher) Unregister(observer Observer) {
for i, obs := range n.observers {
if obs == observer {
n.observers = append(n.observers[:i], n.observers[i+1:]...)
break
}
}
}
func (n *NewsPublisher) Notify(message string) {
for _, observer := range n.observers {
observer.Update(message)
}
}
// Concrete Observers
type EmailSubscriber struct {
name string
}
func (e EmailSubscriber) Update(message string) {
fmt.Printf("Email to %s: %s\n", e.name, message)
}
type SMSSubscriber struct {
phone string
}
func (s SMSSubscriber) Update(message string) {
fmt.Printf("SMS to %s: %s\n", s.phone, message)
}
func main() {
publisher := NewsPublisher{}
emailSub := EmailSubscriber{name: "Alice"}
smsSub := SMSSubscriber{phone: "555-1234"}
publisher.Register(emailSub)
publisher.Register(smsSub)
publisher.Notify("Breaking News: Go 1.18 released!")
publisher.Unregister(emailSub)
publisher.Notify("Update: Go 1.19 coming soon!")
}The Pipeline Pattern processes data through a series of stages. It uses channels for communication between stages.
- Stages: Processing steps
- Channels: Connect stages
- Concurrency: Run stages concurrently
- Fan-out/Fan-in: Parallel processing
- Data Flow: Stream processing
// Pipeline Pattern in Go
package main
import (
"fmt"
"sync"
)
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func filter(in <-chan int, predicate func(int) bool) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if predicate(n) {
out <- n
}
}
close(out)
}()
return out
}
func merge(chans ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chans))
for _, ch := range chans {
go func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
// Pipeline: generate -> square -> filter
nums := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squares := square(nums)
evens := filter(squares, func(n int) bool { return n%2 == 0 })
// Fan-out: multiple workers
workers := 3
chans := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
chans[i] = filter(squares, func(n int) bool { return n%2 == 0 })
}
// Fan-in: merge results
results := merge(chans...)
for result := range results {
fmt.Printf("Result: %d\n", result)
}
}The Worker Pool Pattern manages a pool of workers that process jobs from a queue. It controls concurrency and resource usage.
- Jobs Channel: Work queue
- Workers: Goroutines processing jobs
- Results Channel: Output collection
- WaitGroup: Track completion
- Scalability: Adjust worker count
// Worker Pool Pattern in Go
package main
import (
"fmt"
"sync"
"time"
)
type Job struct {
ID int
}
type Result struct {
JobID int
Output string
}
func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
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("Job %d processed by worker %d", job.ID, id)}
}
}
func main() {
numJobs := 10
numWorkers := 3
jobs := make(chan Job, numJobs)
results := make(chan Result, numJobs)
var wg sync.WaitGroup
// Start workers
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
// Send jobs
for i := 1; i <= numJobs; i++ {
jobs <- Job{ID: i}
}
close(jobs)
// Wait for workers to finish
go func() {
wg.Wait()
close(results)
}()
// Collect results
for result := range results {
fmt.Printf("Result: %s\n", result.Output)
}
}Rate Limiting controls the rate of requests to prevent overload. Token bucket is a common implementation.
- Token Bucket: Leaky bucket algorithm
- Rate: Requests per second
- Burst: Maximum tokens
- Refill: Add tokens periodically
- Usage: API rate limiting
// Rate Limiting in Go
package main
import (
"fmt"
"time"
)
// Token bucket rate limiter
type RateLimiter struct {
tokens chan struct{}
ticker *time.Ticker
}
func NewRateLimiter(rate int) *RateLimiter {
rl := &RateLimiter{
tokens: make(chan struct{}, rate),
ticker: time.NewTicker(time.Second / time.Duration(rate)),
}
// Refill tokens
go func() {
for range rl.ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
// Token bucket full
}
}
}()
// Initialize tokens
for i := 0; i < rate; i++ {
rl.tokens <- struct{}{}
}
return rl
}
func (rl *RateLimiter) Allow() bool {
select {
case <-rl.tokens:
return true
default:
return false
}
}
func (rl *RateLimiter) Stop() {
rl.ticker.Stop()
}
func main() {
limiter := NewRateLimiter(3) // 3 requests per second
defer limiter.Stop()
for i := 0; i < 10; i++ {
if limiter.Allow() {
fmt.Printf("Request %d allowed at %v\n", i, time.Now())
} else {
fmt.Printf("Request %d denied at %v\n", i, time.Now())
}
time.Sleep(200 * time.Millisecond)
}
}The Circuit Breaker Pattern prevents cascading failures by stopping requests to failing services. It has three states: closed, open, half-open.
- Closed: Normal operation
- Open: Service unavailable
- Half-Open: Testing service
- Failure Threshold: Triggers open
- Timeout: Time to try again
// Circuit Breaker Pattern in Go
package main
import (
"errors"
"fmt"
"sync"
"time"
)
type CircuitBreaker struct {
mu sync.Mutex
state string // "closed", "open", "half-open"
failureCount int
maxFailures int
timeout time.Duration
lastFailureTime time.Time
}
func NewCircuitBreaker(maxFailures int, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
state: "closed",
maxFailures: maxFailures,
timeout: timeout,
}
}
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
defer cb.mu.Unlock()
// Check if circuit is open
if cb.state == "open" {
if time.Since(cb.lastFailureTime) > cb.timeout {
cb.state = "half-open"
fmt.Println("Circuit: half-open")
} else {
return errors.New("circuit breaker is open")
}
}
// Execute the function
err := fn()
if err != nil {
cb.failureCount++
cb.lastFailureTime = time.Now()
if cb.failureCount >= cb.maxFailures {
cb.state = "open"
fmt.Println("Circuit: open")
}
return err
}
// Success - reset
cb.failureCount = 0
if cb.state == "half-open" {
cb.state = "closed"
fmt.Println("Circuit: closed")
}
return nil
}
func main() {
cb := NewCircuitBreaker(3, 2*time.Second)
// Simulate a failing service
failCount := 0
for i := 0; i < 10; i++ {
err := cb.Call(func() error {
failCount++
if failCount <= 3 {
return errors.New("service error")
}
return nil
})
if err != nil {
fmt.Printf("Call %d: Error - %v\n", i+1, err)
} else {
fmt.Printf("Call %d: Success\n", i+1)
}
time.Sleep(500 * time.Millisecond)
}
}The Retry Pattern automatically retries failed operations with backoff to handle transient failures.
- Max Attempts: Number of retries
- Backoff: Exponential backoff
- Retry Condition: Which errors to retry
- Jitter: Random delay variation
- Circuit Breaker: Combine with circuit breaker
// Retry Pattern in Go
package main
import (
"errors"
"fmt"
"time"
)
type RetryConfig struct {
MaxAttempts int
InitialDelay time.Duration
MaxDelay time.Duration
Multiplier float64
}
func DefaultRetryConfig() RetryConfig {
return RetryConfig{
MaxAttempts: 3,
InitialDelay: 100 * time.Millisecond,
MaxDelay: 2 * time.Second,
Multiplier: 2.0,
}
}
func Retry(fn func() error, config RetryConfig) error {
var lastErr error
delay := config.InitialDelay
for attempt := 0; attempt < config.MaxAttempts; attempt++ {
err := fn()
if err == nil {
return nil
}
lastErr = err
if attempt < config.MaxAttempts-1 {
fmt.Printf("Attempt %d failed: %v, retrying in %v\n", attempt+1, err, delay)
time.Sleep(delay)
delay = time.Duration(float64(delay) * config.Multiplier)
if delay > config.MaxDelay {
delay = config.MaxDelay
}
}
}
return fmt.Errorf("all %d attempts failed: %w", config.MaxAttempts, lastErr)
}
func main() {
attemptCount := 0
err := Retry(func() error {
attemptCount++
if attemptCount < 3 {
return errors.New("temporary error")
}
return nil
}, DefaultRetryConfig())
if err != nil {
fmt.Printf("Failed: %v\n", err)
} else {
fmt.Printf("Success after %d attempts\n", attemptCount)
}
}The Timeout Pattern sets a deadline for operations to prevent indefinite blocking.
- Context:
context.WithTimeout - Channel: Use select for timeout
- Deadline: Absolute time limit
- Graceful: Clean resource cleanup
- Abort: Cancel operations on timeout
// Timeout Pattern in Go
package main
import (
"context"
"fmt"
"time"
)
func slowOperation() string {
time.Sleep(2 * time.Second)
return "Operation complete"
}
func withTimeout(timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resultCh := make(chan string, 1)
errCh := make(chan error, 1)
go func() {
result := slowOperation()
select {
case resultCh <- result:
case <-ctx.Done():
// Operation completed but context was cancelled
}
}()
select {
case result := <-resultCh:
return result, nil
case <-ctx.Done():
return "", fmt.Errorf("operation timed out after %v", timeout)
}
}
func main() {
// Test with timeout that's too short
result, err := withTimeout(1 * time.Second)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Result: %s\n", result)
}
// Test with sufficient timeout
result2, err2 := withTimeout(3 * time.Second)
if err2 != nil {
fmt.Printf("Error: %v\n", err2)
} else {
fmt.Printf("Result: %s\n", result2)
}
}The Semaphore Pattern limits concurrent access to resources. It's implemented using buffered channels.
- Buffer Channel:
make(chan struct{}, maxConcurrency) - Acquire:
sem <- struct{}{} - Release:
<-sem - Timeout: Timeout on acquire
- Usage: Resource pool, connection limits
// Semaphore Pattern in Go
package main
import (
"fmt"
"sync"
"time"
)
// Semaphore using buffered channel
type Semaphore struct {
ch chan struct{}
}
func NewSemaphore(maxConcurrency int) *Semaphore {
return &Semaphore{
ch: make(chan struct{}, maxConcurrency),
}
}
// Acquire attempts to acquire the semaphore
func (s *Semaphore) Acquire() {
s.ch <- struct{}{}
}
// AcquireWithTimeout attempts to acquire with timeout
func (s *Semaphore) AcquireWithTimeout(timeout time.Duration) bool {
select {
case s.ch <- struct{}{}:
return true
case <-time.After(timeout):
return false
}
}
// Release releases the semaphore
func (s *Semaphore) Release() {
<-s.ch
}
// TryAcquire attempts to acquire without blocking
func (s *Semaphore) TryAcquire() bool {
select {
case s.ch <- struct{}{}:
return true
default:
return false
}
}
// Worker function using semaphore
func worker(id int, sem *Semaphore, wg *sync.WaitGroup) {
defer wg.Done()
// Acquire semaphore
if !sem.AcquireWithTimeout(2 * time.Second) {
fmt.Printf("Worker %d: timeout acquiring semaphore\n", id)
return
}
defer sem.Release()
// Do work
fmt.Printf("Worker %d: acquired semaphore, working...\n", id)
time.Sleep(1 * time.Second)
fmt.Printf("Worker %d: done\n", id)
}
// Example with resource pool
type ResourcePool struct {
sem *Semaphore
resources []int
mu sync.Mutex
}
func NewResourcePool(size int) *ResourcePool {
resources := make([]int, size)
for i := 0; i < size; i++ {
resources[i] = i + 1
}
return &ResourcePool{
sem: NewSemaphore(size),
resources: resources,
}
}
func (rp *ResourcePool) GetResource() (int, bool) {
if !rp.sem.TryAcquire() {
return 0, false
}
rp.mu.Lock()
defer rp.mu.Unlock()
if len(rp.resources) == 0 {
rp.sem.Release()
return 0, false
}
resource := rp.resources[0]
rp.resources = rp.resources[1:]
return resource, true
}
func (rp *ResourcePool) ReleaseResource(resource int) {
rp.mu.Lock()
rp.resources = append(rp.resources, resource)
rp.mu.Unlock()
rp.sem.Release()
}
func main() {
// Example 1: Basic semaphore
sem := NewSemaphore(3) // Allow 3 concurrent operations
var wg sync.WaitGroup
fmt.Println("=== Basic Semaphore ===")
for i := 1; i <= 10; i++ {
wg.Add(1)
go worker(i, sem, &wg)
}
wg.Wait()
// Example 2: Resource pool
fmt.Println("\n=== Resource Pool ===")
pool := NewResourcePool(3)
// Simulate using resources
for i := 0; i < 5; i++ {
go func(id int) {
resource, ok := pool.GetResource()
if !ok {
fmt.Printf("Goroutine %d: no resource available\n", id)
return
}
fmt.Printf("Goroutine %d: acquired resource %d\n", id, resource)
time.Sleep(500 * time.Millisecond)
pool.ReleaseResource(resource)
fmt.Printf("Goroutine %d: released resource %d\n", id, resource)
}(i)
}
time.Sleep(3 * time.Second)
// Example 3: TryAcquire
fmt.Println("\n=== TryAcquire Example ===")
sem2 := NewSemaphore(1)
// Acquire first
if sem2.TryAcquire() {
fmt.Println("First acquisition succeeded")
}
// Try to acquire again (should fail)
if sem2.TryAcquire() {
fmt.Println("Second acquisition succeeded (unexpected)")
} else {
fmt.Println("Second acquisition failed (as expected)")
}
// Release and try again
sem2.Release()
if sem2.TryAcquire() {
fmt.Println("Third acquisition succeeded after release")
}
}Graceful Shutdown allows servers to finish processing requests before stopping. It uses signal handling and context cancellation.
- Signal:
syscall.SIGINT,syscall.SIGTERM - Server Shutdown:
server.Shutdown(ctx) - Timeout: Wait for connections to finish
- Cleanup: Close resources
- Health Checks: Prevent new requests
// Graceful Shutdown in Go
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start server in goroutine
go func() {
fmt.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("Server error: %v\n", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
fmt.Println("Shutting down server...")
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("Server shutdown error: %v\n", err)
}
fmt.Println("Server stopped")
}CORS Middleware enables Cross-Origin Resource Sharing by setting appropriate headers on HTTP responses.
- Headers:
Access-Control-Allow-Origin - Methods: Allowed HTTP methods
- Headers: Allowed request headers
- Preflight: Handle OPTIONS requests
- Configuration: Allow specific origins
// CORS Middleware in Go
package main
import (
"fmt"
"net/http"
)
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"message": "CORS enabled"}`)
})
handler := corsMiddleware(mux)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", handler)
}JWT Authentication uses JSON Web Tokens for stateless authentication. Go provides libraries for token generation and validation.
- Generate:
jwt.NewWithClaims - Validate:
jwt.ParseWithClaims - Claims: Custom claims structure
- Signing Method: HS256, RS256, etc.
- Expiration: Set token expiry
// JWT Authentication in Go
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt"
)
var secretKey = []byte("my-secret-key")
type Claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
func GenerateToken(username string) (string, error) {
expirationTime := time.Now().Add(1 * time.Hour)
claims := &Claims{
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
IssuedAt: time.Now().Unix(),
Issuer: "my-app",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secretKey)
}
func ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return secretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
func main() {
// Generate token
token, err := GenerateToken("alice")
if err != nil {
fmt.Printf("Error generating token: %v\n", err)
return
}
fmt.Printf("Token: %s\n", token)
// Validate token
claims, err := ValidateToken(token)
if err != nil {
fmt.Printf("Error validating token: %v\n", err)
return
}
fmt.Printf("Valid token for user: %s\n", claims.Username)
}Metrics and Monitoring track application performance, errors, and request patterns for observability.
- Requests: Count incoming requests
- Errors: Track error rates
- Duration: Measure response times
- Prometheus: Metrics collection
- Grafana: Visualization
// Metrics and Monitoring in Go
package main
import (
"fmt"
"net/http"
"time"
)
type Metrics struct {
requests int
errors int
totalDuration time.Duration
}
var metrics Metrics
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
duration := time.Since(start)
metrics.requests++
metrics.totalDuration += duration
})
}
func metricsHandler(w http.ResponseWriter, r *http.Request) {
avgDuration := metrics.totalDuration / time.Duration(metrics.requests)
fmt.Fprintf(w, "Requests: %d\n", metrics.requests)
fmt.Fprintf(w, "Errors: %d\n", metrics.errors)
fmt.Fprintf(w, "Avg Duration: %v\n", avgDuration)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(100 * time.Millisecond)
fmt.Fprintf(w, "API response")
})
mux.HandleFunc("/metrics", metricsHandler)
handler := metricsMiddleware(mux)
http.ListenAndServe(":8080", handler)
}Profiling analyzes program performance to identify bottlenecks. Go provides CPU and memory profiling.
- CPU Profile:
pprof.StartCPUProfile - Memory Profile:
pprof.WriteHeapProfile - Web UI:
go tool pprof - Flame Graphs: Visual representation
- Optimization: Identify hotspots
// Profiling in Go
package main
import (
"fmt"
"os"
"runtime/pprof"
"time"
)
func expensiveOperation() {
sum := 0
for i := 0; i < 1000000; i++ {
sum += i
}
}
func main() {
// CPU profiling
f, err := os.Create("cpu.prof")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run operations
for i := 0; i < 10; i++ {
expensiveOperation()
time.Sleep(10 * time.Millisecond)
}
// Memory profiling
f2, err := os.Create("mem.prof")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer f2.Close()
pprof.WriteHeapProfile(f2)
fmt.Println("Profiling complete")
}Tracing tracks the flow of requests through a distributed system. It provides visibility into service interactions.
- Spans: Individual operations
- Trace ID: Unique request identifier
- Parent/Child: Span relationships
- Tags: Additional metadata
- Jaeger: Distributed tracing system
// Tracing in Go
package main
import (
"context"
"fmt"
"time"
)
type Trace struct {
name string
start time.Time
end time.Time
children []*Trace
}
func StartTrace(name string) *Trace {
return &Trace{
name: name,
start: time.Now(),
children: []*Trace{},
}
}
func (t *Trace) End() {
t.end = time.Now()
}
func (t *Trace) AddChild(child *Trace) {
t.children = append(t.children, child)
}
func (t *Trace) Duration() time.Duration {
return t.end.Sub(t.start)
}
func (t *Trace) Print(indent int) {
prefix := ""
for i := 0; i < indent; i++ {
prefix += " "
}
fmt.Printf("%s%s: %v\n", prefix, t.name, t.Duration())
for _, child := range t.children {
child.Print(indent + 1)
}
}
func operation1(ctx context.Context, trace *Trace) {
child := StartTrace("operation1")
defer child.End()
trace.AddChild(child)
time.Sleep(100 * time.Millisecond)
operation2(ctx, child)
}
func operation2(ctx context.Context, trace *Trace) {
child := StartTrace("operation2")
defer child.End()
trace.AddChild(child)
time.Sleep(50 * time.Millisecond)
operation3(ctx, child)
}
func operation3(ctx context.Context, trace *Trace) {
child := StartTrace("operation3")
defer child.End()
trace.AddChild(child)
time.Sleep(25 * time.Millisecond)
}
func main() {
ctx := context.Background()
root := StartTrace("main")
defer root.End()
operation1(ctx, root)
root.Print(0)
}Feature Flags enable dynamic feature control, allowing gradual rollouts and A/B testing.
- Enable/Disable: Control features
- Configuration: Runtime configuration
- Rollout: Gradual feature release
- Testing: A/B testing support
- Fallback: Safe defaults
// Feature Flags in Go
package main
import (
"fmt"
"sync"
)
type FeatureFlags struct {
mu sync.RWMutex
flags map[string]bool
}
func NewFeatureFlags() *FeatureFlags {
return &FeatureFlags{
flags: make(map[string]bool),
}
}
func (ff *FeatureFlags) SetFlag(name string, enabled bool) {
ff.mu.Lock()
defer ff.mu.Unlock()
ff.flags[name] = enabled
}
func (ff *FeatureFlags) IsEnabled(name string) bool {
ff.mu.RLock()
defer ff.mu.RUnlock()
return ff.flags[name]
}
func (ff *FeatureFlags) GetFlags() map[string]bool {
ff.mu.RLock()
defer ff.mu.RUnlock()
result := make(map[string]bool)
for k, v := range ff.flags {
result[k] = v
}
return result
}
func main() {
ff := NewFeatureFlags()
ff.SetFlag("new-feature", true)
ff.SetFlag("experimental-api", false)
if ff.IsEnabled("new-feature") {
fmt.Println("New feature is enabled")
}
if !ff.IsEnabled("experimental-api") {
fmt.Println("Experimental API is disabled")
}
}Configuration Management loads and manages application settings from files, environment variables, and command-line flags.
- JSON/YAML: Configuration files
- Environment Variables: Runtime overrides
- Struct Tags: Configuration mapping
- Default Values: Fallback values
- Validation: Validate configuration
// Configuration Management in Go
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
)
type Config struct {
Server struct {
Host string `json:"host"`
Port int `json:"port"`
} `json:"server"`
Database struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
} `json:"database"`
Logging struct {
Level string `json:"level"`
Output string `json:"output"`
} `json:"logging"`
}
func LoadConfig(path string) (*Config, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
var config Config
err = json.Unmarshal(data, &config)
if err != nil {
return nil, err
}
// Environment variable overrides
if host := os.Getenv("DB_HOST"); host != "" {
config.Database.Host = host
}
if port := os.Getenv("DB_PORT"); port != "" {
fmt.Sscanf(port, "%d", &config.Database.Port)
}
return &config, nil
}
func main() {
// Create sample config file
sampleConfig := Config{
Server: struct {
Host string `json:"host"`
Port int `json:"port"`
}{Host: "localhost", Port: 8080},
Database: struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
}{Host: "localhost", Port: 5432, Username: "user", Password: "pass", Name: "db"},
Logging: struct {
Level string `json:"level"`
Output string `json:"output"`
}{Level: "info", Output: "stdout"},
}
data, _ := json.MarshalIndent(sampleConfig, "", " ")
ioutil.WriteFile("config.json", data, 0644)
// Load config
config, err := LoadConfig("config.json")
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
fmt.Printf("Server: %s:%d\n", config.Server.Host, config.Server.Port)
fmt.Printf("Database: %s:%d/%s\n", config.Database.Host, config.Database.Port, config.Database.Name)
}Environment Variables provide a way to configure applications at runtime using the os package.
- Get:
os.Getenv("KEY") - Default: Provide fallback values
- Parse: Convert to appropriate types
- Validation: Validate required variables
- 12-Factor: Config via environment
// Environment Variables Configuration in Go
package main
import (
"fmt"
"os"
"strconv"
)
type AppConfig struct {
Port int
DatabaseURL string
LogLevel string
MaxConnections int
}
func LoadConfigFromEnv() AppConfig {
config := AppConfig{
Port: 8080,
DatabaseURL: "postgres://localhost:5432",
LogLevel: "info",
MaxConnections: 10,
}
if port := os.Getenv("PORT"); port != "" {
if p, err := strconv.Atoi(port); err == nil {
config.Port = p
}
}
if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" {
config.DatabaseURL = dbURL
}
if logLevel := os.Getenv("LOG_LEVEL"); logLevel != "" {
config.LogLevel = logLevel
}
if maxConn := os.Getenv("MAX_CONNECTIONS"); maxConn != "" {
if m, err := strconv.Atoi(maxConn); err == nil {
config.MaxConnections = m
}
}
return config
}
func main() {
os.Setenv("PORT", "9090")
os.Setenv("LOG_LEVEL", "debug")
config := LoadConfigFromEnv()
fmt.Printf("Port: %d\n", config.Port)
fmt.Printf("DatabaseURL: %s\n", config.DatabaseURL)
fmt.Printf("LogLevel: %s\n", config.LogLevel)
fmt.Printf("MaxConnections: %d\n", config.MaxConnections)
}Versioning in Go uses build-time variables to embed version information into binaries.
- Build Tags:
-ldflags "-X main.Version=v1.0.0" - Constants: Define version constants
- Git Tags: Version from Git tags
- Semantic Versioning: Major.Minor.Patch
- API: Version endpoint for API
// Versioning in Go
package main
import (
"fmt"
"runtime"
)
// Build information (set by ldflags)
var (
Version = "dev"
BuildTime = "unknown"
GitCommit = "unknown"
)
func main() {
fmt.Printf("Application: %s\n", Version)
fmt.Printf("Build Time: %s\n", BuildTime)
fmt.Printf("Git Commit: %s\n", GitCommit)
fmt.Printf("Go Version: %s\n", runtime.Version())
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}Docker Integration involves creating container images for Go applications using Dockerfiles.
- Dockerfile: Build image
- Multi-stage Builds: Smaller images
- Alpine: Minimal base images
- Environment: Container configuration
- Health Checks: Container health monitoring
// Docker Integration in Go
package main
import (
"fmt"
"os"
)
func main() {
// Check if running in container
if _, err := os.Stat("/.dockerenv"); err == nil {
fmt.Println("Running in Docker container")
}
// Get container ID from cgroup
if data, err := os.ReadFile("/proc/self/cgroup"); err == nil {
fmt.Printf("Cgroup: %s\n", string(data[:100]))
}
// Container environment variables
hostname, _ := os.Hostname()
fmt.Printf("Hostname: %s\n", hostname)
fmt.Println("Application running")
}Health Check Endpoints provide status information for monitoring and container orchestration systems.
- /health: Overall health status
- /ready: Readiness for traffic
- /live: Liveness check
- JSON Response: Structured status data
- Service Checks: Database, cache, etc.
// Health Check Endpoint in Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
type HealthStatus struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Uptime string `json:"uptime"`
Services map[string]string `json:"services"`
}
var startTime = time.Now()
func healthHandler(w http.ResponseWriter, r *http.Request) {
status := HealthStatus{
Status: "healthy",
Timestamp: time.Now().Format(time.RFC3339),
Uptime: time.Since(startTime).String(),
Services: map[string]string{
"database": "healthy",
"cache": "healthy",
"api": "healthy",
},
}
// Check services (simplified)
// In real app, check actual service health
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status)
}
func readinessHandler(w http.ResponseWriter, r *http.Request) {
// Check if application is ready to serve traffic
ready := true
if !ready {
http.Error(w, "Not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ready"}`))
}
func livenessHandler(w http.ResponseWriter, r *http.Request) {
// Check if application is alive
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "alive"}`))
}
func main() {
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/ready", readinessHandler)
http.HandleFunc("/live", livenessHandler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}Graceful Restart allows restarting a server without downtime by starting a new process before stopping the old one.
- Process Replacement: Start new process
- Socket Transfer: Pass listening sockets
- Signal Handling: Handle restart signals
- Zero Downtime: No request interruption
- Lock Files: Prevent multiple instances
// Graceful Restart in Go
package main
import (
"fmt"
"net"
"os"
"os/exec"
"syscall"
)
func main() {
fmt.Printf("PID: %d\n", os.Getpid())
// Check if this is a restart
if len(os.Args) > 1 && os.Args[1] == "restart" {
fmt.Println("Restarting...")
cmd := exec.Command(os.Args[0])
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGTERM,
}
cmd.Start()
fmt.Printf("New process started: %d\n", cmd.Process.Pid)
return
}
// Listen on port
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer listener.Close()
fmt.Println("Server running on :8080")
// Keep running
select {}
}Service Discovery allows services to find each other in distributed systems. It registers and queries service instances.
- Registry: Service registration
- Health Checks: Heartbeat monitoring
- Load Balancing: Distribute requests
- Consul: Service discovery tool
- etcd: Distributed key-value store
// Service Discovery in Go
package main
import (
"fmt"
"sync"
"time"
)
type ServiceInstance struct {
ID string
Name string
Address string
Port int
HealthCheckURL string
LastSeen time.Time
}
type ServiceRegistry struct {
mu sync.RWMutex
services map[string][]ServiceInstance
}
func NewServiceRegistry() *ServiceRegistry {
return &ServiceRegistry{
services: make(map[string][]ServiceInstance),
}
}
func (sr *ServiceRegistry) Register(service ServiceInstance) {
sr.mu.Lock()
defer sr.mu.Unlock()
service.LastSeen = time.Now()
sr.services[service.Name] = append(sr.services[service.Name], service)
fmt.Printf("Registered service: %s (ID: %s)\n", service.Name, service.ID)
}
func (sr *ServiceRegistry) Deregister(name, id string) {
sr.mu.Lock()
defer sr.mu.Unlock()
instances := sr.services[name]
for i, svc := range instances {
if svc.ID == id {
sr.services[name] = append(instances[:i], instances[i+1:]...)
fmt.Printf("Deregistered service: %s (ID: %s)\n", name, id)
return
}
}
}
func (sr *ServiceRegistry) GetInstances(name string) []ServiceInstance {
sr.mu.RLock()
defer sr.mu.RUnlock()
// Remove stale services
var active []ServiceInstance
for _, svc := range sr.services[name] {
if time.Since(svc.LastSeen) < 30*time.Second {
active = append(active, svc)
}
}
return active
}
func main() {
registry := NewServiceRegistry()
// Register services
registry.Register(ServiceInstance{
ID: "1",
Name: "user-service",
Address: "localhost",
Port: 8081,
})
registry.Register(ServiceInstance{
ID: "2",
Name: "user-service",
Address: "localhost",
Port: 8082,
})
registry.Register(ServiceInstance{
ID: "3",
Name: "order-service",
Address: "localhost",
Port: 8083,
})
// Get instances
userServices := registry.GetInstances("user-service")
fmt.Printf("User services: %v\n", userServices)
orderServices := registry.GetInstances("order-service")
fmt.Printf("Order services: %v\n", orderServices)
}Circuit Breaker with Metrics tracks request success/failure rates and opens the circuit based on metrics.
- Metrics Collection: Track successes and failures
- Threshold: Failure rate threshold
- Time Window: Rolling window metrics
- State Transitions: Closed, open, half-open
- Monitoring: Export metrics for monitoring
// Circuit Breaker with Metrics in Go
package main
import (
"errors"
"fmt"
"sync/atomic"
"time"
)
type Metrics struct {
totalRequests int64
successRequests int64
failureRequests int64
}
type CircuitBreakerWithMetrics struct {
metrics Metrics
state int32 // 0=closed, 1=open, 2=half-open
failureThreshold int64
timeout time.Duration
lastFailure time.Time
mu sync.Mutex
}
func NewCircuitBreakerWithMetrics(threshold int64, timeout time.Duration) *CircuitBreakerWithMetrics {
return &CircuitBreakerWithMetrics{
failureThreshold: threshold,
timeout: timeout,
}
}
func (cb *CircuitBreakerWithMetrics) Call(fn func() error) error {
atomic.AddInt64(&cb.metrics.totalRequests, 1)
cb.mu.Lock()
if cb.state == 1 { // open
if time.Since(cb.lastFailure) > cb.timeout {
cb.state = 2 // half-open
cb.mu.Unlock()
fmt.Println("Circuit: half-open")
} else {
cb.mu.Unlock()
atomic.AddInt64(&cb.metrics.failureRequests, 1)
return errors.New("circuit breaker is open")
}
} else {
cb.mu.Unlock()
}
err := fn()
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
atomic.AddInt64(&cb.metrics.failureRequests, 1)
cb.lastFailure = time.Now()
if cb.metrics.failureRequests >= cb.failureThreshold {
cb.state = 1 // open
fmt.Println("Circuit: open")
}
return err
}
atomic.AddInt64(&cb.metrics.successRequests, 1)
if cb.state == 2 {
cb.state = 0 // closed
fmt.Println("Circuit: closed")
}
return nil
}
func (cb *CircuitBreakerWithMetrics) GetMetrics() Metrics {
return cb.metrics
}
func main() {
cb := NewCircuitBreakerWithMetrics(3, 2*time.Second)
for i := 0; i < 10; i++ {
err := cb.Call(func() error {
if i < 4 {
return errors.New("service error")
}
return nil
})
if err != nil {
fmt.Printf("Call %d: Error - %v\n", i+1, err)
} else {
fmt.Printf("Call %d: Success\n", i+1)
}
time.Sleep(500 * time.Millisecond)
}
metrics := cb.GetMetrics()
fmt.Printf("Metrics: Total=%d, Success=%d, Failure=%d\n",
metrics.totalRequests, metrics.successRequests, metrics.failureRequests)
}Distributed Tracing tracks requests across multiple services, providing end-to-end visibility.
- Trace ID: Unique request identifier
- Spans: Individual operations
- Context Propagation: Pass trace context
- Exporters: Jaeger, Zipkin, etc.
- Instrumentation: Add tracing to code
// Distributed Tracing in Go
package main
import (
"context"
"fmt"
"time"
)
type Span struct {
ID string
ParentID string
Name string
Start time.Time
End time.Time
Tags map[string]string
}
type Tracer struct {
spans []*Span
}
func NewTracer() *Tracer {
return &Tracer{
spans: []*Span{},
}
}
func (t *Tracer) StartSpan(ctx context.Context, name string) (*Span, context.Context) {
span := &Span{
ID: fmt.Sprintf("%d", len(t.spans)+1),
Name: name,
Start: time.Now(),
Tags: make(map[string]string),
}
// Get parent ID from context
if parent, ok := ctx.Value("span").(*Span); ok {
span.ParentID = parent.ID
}
t.spans = append(t.spans, span)
ctx = context.WithValue(ctx, "span", span)
return span, ctx
}
func (t *Tracer) EndSpan(span *Span) {
span.End = time.Now()
}
func (t *Tracer) AddTag(span *Span, key, value string) {
span.Tags[key] = value
}
func (t *Tracer) PrintSpans() {
for _, span := range t.spans {
duration := span.End.Sub(span.Start)
parent := "root"
if span.ParentID != "" {
parent = span.ParentID
}
fmt.Printf("Span: %s, Parent: %s, Duration: %v, Tags: %v\n",
span.Name, parent, duration, span.Tags)
}
}
func main() {
tracer := NewTracer()
ctx := context.Background()
// Root span
span1, ctx := tracer.StartSpan(ctx, "main")
tracer.AddTag(span1, "service", "api-gateway")
// Child span
span2, ctx := tracer.StartSpan(ctx, "database-query")
tracer.AddTag(span2, "table", "users")
time.Sleep(50 * time.Millisecond)
tracer.EndSpan(span2)
// Another child span
span3, ctx := tracer.StartSpan(ctx, "external-api")
tracer.AddTag(span3, "endpoint", "/api/data")
time.Sleep(100 * time.Millisecond)
tracer.EndSpan(span3)
tracer.EndSpan(span1)
tracer.PrintSpans()
}API Versioning manages multiple API versions simultaneously to support backward compatibility.
- URL Path:
/api/v1/users - Header:
API-Version: v1 - Query Parameter:
?version=v1 - Deprecation: Mark older versions
- Migration: Gradual version migration
// API Versioning in Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
type UserV1 struct {
ID int `json:"id"`
Name string `json:"name"`
}
type UserV2 struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
version := "v1"
// Extract version from path
if strings.Contains(path, "/v2/") {
version = "v2"
}
// Also check header
if v := r.Header.Get("API-Version"); v != "" {
version = v
}
switch version {
case "v1":
users := []UserV1{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
case "v2":
users := []UserV2{
{ID: 1, Name: "Alice", Email: "alice@email.com", Age: 25},
{ID: 2, Name: "Bob", Email: "bob@email.com", Age: 30},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
default:
http.Error(w, "Unsupported version", http.StatusBadRequest)
}
}
func main() {
http.HandleFunc("/api/v1/users", apiHandler)
http.HandleFunc("/api/v2/users", apiHandler)
http.HandleFunc("/api/users", apiHandler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}Pagination splits large result sets into smaller pages for better performance and user experience.
- Page/PerPage: Current page and items per page
- Offset/Limit: SQL pagination
- Total Pages: Calculate total pages
- Navigation: Next, previous, first, last
- Performance: Use LIMIT/OFFSET
// Pagination in Go
package main
import (
"fmt"
"math"
"strconv"
)
type Pagination struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
}
func Paginate(items []string, page, perPage int) ([]string, Pagination) {
total := len(items)
totalPages := int(math.Ceil(float64(total) / float64(perPage)))
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 10
}
start := (page - 1) * perPage
end := start + perPage
if start >= total {
return []string{}, Pagination{
Page: page,
PerPage: perPage,
Total: total,
TotalPages: totalPages,
}
}
if end > total {
end = total
}
pagination := Pagination{
Page: page,
PerPage: perPage,
Total: total,
TotalPages: totalPages,
}
return items[start:end], pagination
}
func main() {
items := make([]string, 0)
for i := 1; i <= 100; i++ {
items = append(items, fmt.Sprintf("Item %d", i))
}
page := 2
perPage := 10
result, pagination := Paginate(items, page, perPage)
fmt.Printf("Page %d of %d\n", pagination.Page, pagination.TotalPages)
fmt.Printf("Items: %v\n", result)
fmt.Printf("Pagination: %+v\n", pagination)
}Sorting and Filtering organizes and filters data based on specified criteria.
- Sort:
sort.Slicewith custom comparator - Filter: Conditional filtering
- Multi-field: Sort by multiple fields
- ASC/DESC: Ascending/descending order
- Query Parameters: Sort and filter from requests
// Sorting and Filtering in Go
package main
import (
"fmt"
"sort"
"strings"
)
type User struct {
ID int
Name string
Age int
Email string
}
type UserFilter struct {
NameContains string
MinAge int
MaxAge int
SortBy string
SortDesc bool
}
func FilterUsers(users []User, filter UserFilter) []User {
result := make([]User, 0)
for _, user := range users {
// Filter by name
if filter.NameContains != "" && !strings.Contains(strings.ToLower(user.Name), strings.ToLower(filter.NameContains)) {
continue
}
// Filter by age
if filter.MinAge > 0 && user.Age < filter.MinAge {
continue
}
if filter.MaxAge > 0 && user.Age > filter.MaxAge {
continue
}
result = append(result, user)
}
// Sort
if filter.SortBy != "" {
sort.Slice(result, func(i, j int) bool {
var less bool
switch filter.SortBy {
case "name":
less = result[i].Name < result[j].Name
case "age":
less = result[i].Age < result[j].Age
case "id":
less = result[i].ID < result[j].ID
default:
less = result[i].ID < result[j].ID
}
if filter.SortDesc {
return !less
}
return less
})
}
return result
}
func main() {
users := []User{
{ID: 3, Name: "Carol", Age: 22, Email: "carol@email.com"},
{ID: 1, Name: "Alice", Age: 25, Email: "alice@email.com"},
{ID: 4, Name: "Dave", Age: 35, Email: "dave@email.com"},
{ID: 2, Name: "Bob", Age: 30, Email: "bob@email.com"},
}
filter := UserFilter{
NameContains: "a",
MinAge: 20,
MaxAge: 30,
SortBy: "name",
SortDesc: false,
}
result := FilterUsers(users, filter)
for _, user := range result {
fmt.Printf("%+v\n", user)
}
}Search finds relevant documents based on query terms with relevance scoring.
- Term Search: Match query terms
- Relevance: Score matching documents
- Ranking: Sort by relevance
- Full-Text: Index and search text
- Elasticsearch: Search engine integration
// Search in Go
package main
import (
"fmt"
"strings"
)
type Document struct {
ID int
Title string
Content string
}
type SearchResult struct {
Document Document
Score int
}
func Search(documents []Document, query string) []SearchResult {
results := make([]SearchResult, 0)
query = strings.ToLower(query)
queryWords := strings.Fields(query)
for _, doc := range documents {
score := 0
content := strings.ToLower(doc.Title + " " + doc.Content)
for _, word := range queryWords {
if strings.Contains(content, word) {
score++
}
}
if score > 0 {
results = append(results, SearchResult{Document: doc, Score: score})
}
}
// Sort by score (descending)
for i := 0; i < len(results); i++ {
for j := i + 1; j < len(results); j++ {
if results[j].Score > results[i].Score {
results[i], results[j] = results[j], results[i]
}
}
}
return results
}
func main() {
documents := []Document{
{ID: 1, Title: "Go Programming", Content: "Go is a programming language designed for simplicity"},
{ID: 2, Title: "Web Development", Content: "Web development with Go is fast and efficient"},
{ID: 3, Title: "Database Systems", Content: "Go has excellent database drivers and ORM support"},
}
results := Search(documents, "go programming")
fmt.Println("Search results:")
for _, result := range results {
fmt.Printf("Doc %d: %s (Score: %d)\n",
result.Document.ID, result.Document.Title, result.Score)
}
}Caching stores frequently accessed data to improve performance. Go implements caching with TTL and eviction policies.
- In-Memory: Memory-based cache
- TTL: Time-to-live for entries
- LRU: Least Recently Used eviction
- Redis: Distributed caching
- Cache-Aside: Read-through pattern
// Caching in Go
package main
import (
"fmt"
"sync"
"time"
)
type CacheItem struct {
Value interface{}
Expiration time.Time
}
type Cache struct {
mu sync.RWMutex
items map[string]CacheItem
defaultTTL time.Duration
cleanupInterval time.Duration
}
func NewCache(defaultTTL, cleanupInterval time.Duration) *Cache {
cache := &Cache{
items: make(map[string]CacheItem),
defaultTTL: defaultTTL,
cleanupInterval: cleanupInterval,
}
// Start cleanup goroutine
go cache.cleanup()
return cache
}
func (c *Cache) Set(key string, value interface{}, ttl ...time.Duration) {
expiration := time.Now().Add(c.defaultTTL)
if len(ttl) > 0 {
expiration = time.Now().Add(ttl[0])
}
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = CacheItem{Value: value, Expiration: expiration}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
item, exists := c.items[key]
c.mu.RUnlock()
if !exists {
return nil, false
}
if time.Now().After(item.Expiration) {
c.Delete(key)
return nil, false
}
return item.Value, true
}
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func (c *Cache) cleanup() {
ticker := time.NewTicker(c.cleanupInterval)
defer ticker.Stop()
for range ticker.C {
c.mu.Lock()
for key, item := range c.items {
if time.Now().After(item.Expiration) {
delete(c.items, key)
}
}
c.mu.Unlock()
}
}
func main() {
cache := NewCache(5*time.Second, 1*time.Second)
cache.Set("key1", "value1")
cache.Set("key2", "value2", 2*time.Second)
// Get immediately
if val, ok := cache.Get("key1"); ok {
fmt.Printf("key1: %v\n", val)
}
// Wait for expiration
time.Sleep(3 * time.Second)
if val, ok := cache.Get("key2"); ok {
fmt.Printf("key2: %v\n", val)
} else {
fmt.Println("key2 expired")
}
}Token Bucket Rate Limiting controls request rates by consuming tokens from a bucket that refills over time.
- Tokens: Available tokens
- Refill: Tokens added periodically
- Consume: Tokens removed per request
- Wait: Block until token available
- Burst: Maximum token capacity
// Rate Limiting with Token Bucket in Go
package main
import (
"fmt"
"sync"
"time"
)
type TokenBucket struct {
mu sync.Mutex
tokens int
maxTokens int
refillRate time.Duration
lastRefill time.Time
}
func NewTokenBucket(maxTokens int, refillRate time.Duration) *TokenBucket {
return &TokenBucket{
tokens: maxTokens,
maxTokens: maxTokens,
refillRate: refillRate,
lastRefill: time.Now(),
}
}
func (tb *TokenBucket) refill() {
now := time.Now()
elapsed := now.Sub(tb.lastRefill)
tokensToAdd := int(elapsed / tb.refillRate)
if tokensToAdd > 0 {
tb.tokens = tb.tokens + tokensToAdd
if tb.tokens > tb.maxTokens {
tb.tokens = tb.maxTokens
}
tb.lastRefill = now
}
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
tb.refill()
if tb.tokens > 0 {
tb.tokens--
return true
}
return false
}
func (tb *TokenBucket) Wait() {
for !tb.Allow() {
time.Sleep(10 * time.Millisecond)
}
}
func main() {
tb := NewTokenBucket(5, 100*time.Millisecond)
for i := 0; i < 10; i++ {
if tb.Allow() {
fmt.Printf("Request %d allowed at %v\n", i+1, time.Now())
} else {
fmt.Printf("Request %d denied at %v\n", i+1, time.Now())
}
time.Sleep(50 * time.Millisecond)
}
}Context with Values and Timeout combines request-scoped data with deadlines for complete request management.
- Values: Request-scoped data
- Timeout: Deadline for operations
- Cancellation: Cancel operations
- Propagation: Pass context through API
- Cleanup: Defer cancel for cleanup
// Context with Values and Timeout in Go
package main
import (
"context"
"fmt"
"time"
)
type contextKey string
func processRequest(ctx context.Context) {
// Get values from context
userID := ctx.Value(contextKey("userID"))
requestID := ctx.Value(contextKey("requestID"))
if userID == nil || requestID == nil {
fmt.Println("Missing context values")
return
}
fmt.Printf("Processing request: userID=%v, requestID=%v\n", userID, requestID)
// Simulate work with timeout
select {
case <-ctx.Done():
fmt.Println("Request cancelled: ", ctx.Err())
return
case <-time.After(2 * time.Second):
fmt.Println("Request completed successfully")
}
}
func main() {
// Create context with values
ctx := context.Background()
ctx = context.WithValue(ctx, contextKey("userID"), "12345")
ctx = context.WithValue(ctx, contextKey("requestID"), "req-abc-123")
// Add timeout
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
processRequest(ctx)
}Database Transactions ensure atomicity of multiple database operations using the Tx interface.
- Begin: Start transaction
- Exec: Execute operations
- Commit: Commit changes
- Rollback: Rollback on error
- Isolation: Transaction isolation levels
// Database Transaction in Go
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// Connect to database
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer db.Close()
// Start transaction
tx, err := db.Begin()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Perform operations
_, err = tx.Exec("INSERT INTO users (name, age) VALUES (?, ?)", "Alice", 25)
if err != nil {
tx.Rollback()
fmt.Printf("Error: %v\n", err)
return
}
_, err = tx.Exec("INSERT INTO users (name, age) VALUES (?, ?)", "Bob", 30)
if err != nil {
tx.Rollback()
fmt.Printf("Error: %v\n", err)
return
}
// Commit transaction
err = tx.Commit()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Transaction completed successfully")
}Database Migration manages schema changes over time using versioned migration scripts.
- Migration Table: Track applied migrations
- Up/Down: Apply and rollback
- Version Control: Track migration scripts
- Tools: goose, golang-migrate
- Idempotent: Safe to run multiple times
// Database Migration in Go
package main
import (
"database/sql"
"fmt"
"log"
)
type Migration struct {
Version int
Name string
Up string
Down string
}
func MigrateUp(db *sql.DB, migrations []Migration) error {
// Create migrations table
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
return err
}
// Get current version
var currentVersion int
row := db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM migrations")
row.Scan(¤tVersion)
// Apply migrations
for _, migration := range migrations {
if migration.Version > currentVersion {
log.Printf("Applying migration %d: %s", migration.Version, migration.Name)
_, err := db.Exec(migration.Up)
if err != nil {
return fmt.Errorf("migration %d failed: %v", migration.Version, err)
}
_, err = db.Exec("INSERT INTO migrations (version, name) VALUES (?, ?)",
migration.Version, migration.Name)
if err != nil {
return err
}
log.Printf("Migration %d complete", migration.Version)
}
}
return nil
}
func main() {
// db, err := sql.Open("sqlite3", "app.db")
// if err != nil {
// log.Fatalf("Failed to connect: %v", err)
// }
// defer db.Close()
// migrations := []Migration{
// {Version: 1, Name: "create_users", Up: "CREATE TABLE users...", Down: "DROP TABLE users"},
// }
// if err := MigrateUp(db, migrations); err != nil {
// log.Fatalf("Migration failed: %v", err)
// }
log.Println("Migration tool ready")
}API Client provides a structured way to interact with external APIs with proper error handling and configuration.
- Base URL: API endpoint
- Headers: Authentication and content type
- Methods: GET, POST, PUT, DELETE
- JSON: Marshal and unmarshal
- Error Handling: Handle API errors
// API Client in Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
type APIClient struct {
BaseURL string
Client *http.Client
APIKey string
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type Response struct {
Data interface{} `json:"data"`
Message string `json:"message"`
Status string `json:"status"`
}
func NewAPIClient(baseURL, apiKey string) *APIClient {
return &APIClient{
BaseURL: baseURL,
Client: &http.Client{
Timeout: 10 * time.Second,
},
APIKey: apiKey,
}
}
func (c *APIClient) Get(path string, result interface{}) error {
url := c.BaseURL + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.Client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(body, result)
}
func (c *APIClient) Post(path string, data interface{}, result interface{}) error {
url := c.BaseURL + path
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.Client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(body, result)
}
func main() {
client := NewAPIClient("https://api.example.com", "your-api-key")
// GET request
var users []User
err := client.Get("/users", &users)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Users: %+v\n", users)
}
// POST request
newUser := User{Name: "Alice", Email: "alice@email.com"}
var response Response
err = client.Post("/users", newUser, &response)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Response: %+v\n", response)
}
}A Complete Web Application in Go demonstrates full-stack development with routing, middleware, database, and graceful shutdown.
- Routing: HTTP routing with handlers
- Middleware: Logging, CORS, auth
- Database: Store and retrieve data
- Models: Data structures
- Graceful Shutdown: Clean server shutdown
// Complete Web Application in Go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"context"
)
// Models
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
// In-memory store
type Store struct {
mu sync.RWMutex
users map[int]User
nextID int
}
func NewStore() *Store {
return &Store{
users: make(map[int]User),
nextID: 1,
}
}
func (s *Store) CreateUser(name, email string) User {
s.mu.Lock()
defer s.mu.Unlock()
user := User{
ID: s.nextID,
Name: name,
Email: email,
CreatedAt: time.Now(),
}
s.users[user.ID] = user
s.nextID++
return user
}
func (s *Store) GetUsers() []User {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]User, 0, len(s.users))
for _, user := range s.users {
users = append(users, user)
}
return users
}
func (s *Store) GetUser(id int) (User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[id]
return user, ok
}
// Handlers
func handleUsers(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
users := store.GetUsers()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
case "POST":
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
created := store.CreateUser(user.Name, user.Email)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(created)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
}
func handleUser(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Extract ID from URL
// Simplified: expect ID in query param
idStr := r.URL.Query().Get("id")
if idStr == "" {
http.Error(w, "ID required", http.StatusBadRequest)
return
}
var id int
fmt.Sscanf(idStr, "%d", &id)
user, ok := store.GetUser(id)
if !ok {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
}
// Middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("%s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("Completed in %v", time.Since(start))
})
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
store := NewStore()
// Add sample data
store.CreateUser("Alice", "alice@email.com")
store.CreateUser("Bob", "bob@email.com")
// Router
mux := http.NewServeMux()
mux.HandleFunc("/api/users", handleUsers(store))
mux.HandleFunc("/api/user", handleUser(store))
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "healthy"}`))
})
// Apply middleware
handler := loggingMiddleware(corsMiddleware(mux))
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
// Graceful shutdown
go func() {
log.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
server.Shutdown(context.Background())
log.Println("Server stopped")
}Quick Navigation
Related Topics
Ready for your next interview?
Take a real-time mock interview and boost your confidence.
Start Mock Interview