- Published on
Go Language Complete Guide 2025: From Goroutines, Channels, Interfaces to Production Patterns
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction: Why Go in 2025?
- 1. Go Fundamentals: Types, Structs, Methods, Pointers
- 2. Interfaces and Composition
- 3. Concurrency: Goroutines and Channels
- 4. Concurrency Patterns
- 5. Error Handling
- 6. Generics (Go 1.18+)
- 7. Testing
- 8. Building REST APIs
- 9. Building gRPC Services
- 10. CLI Tool Development
- 11. Production Deployment
- 12. Go vs Rust vs Java Comparison
- 13. Interview Questions (15)
- 14. Practice Quiz
- References
Introduction: Why Go in 2025?
Kubernetes, Docker, Terraform, Prometheus, Istio — the core tools of the cloud-native ecosystem are all written in Go. In the 2025 Stack Overflow Survey, Go ranked 3rd in "most wanted languages" and 4th in "highest-paying languages," while Go developer job postings increased 15% year-over-year on Indeed.
The reasons Go gets chosen are clear:
- Simplicity: 25 keywords, no classes, no inheritance
- Concurrency: Millions of concurrent tasks with goroutines and channels
- Fast compilation: Millions of lines compile in seconds
- Single binary: Static binaries with zero dependencies make deployment trivially simple
- Performance: Near C/C++ execution speed, more memory-efficient than Java/Python
Companies and Projects Using Go:
┌───────────────────────────────────────────────────────────┐
│ Google - Internal infra, gRPC, original K8s authors │
│ Uber - High-perf geofencing service (rewrite) │
│ Cloudflare - Edge proxies, DNS, Workers runtime │
│ Twitch - Real-time chat (millions of connections) │
│ Docker - Entire container runtime in Go │
│ Terraform - All HashiCorp products in Go │
│ CockroachDB - Distributed SQL database │
│ Dropbox - Migrated perf-critical services from Python│
└───────────────────────────────────────────────────────────┘
This guide systematically covers Go from basic syntax to concurrency patterns, production-level server construction, CLI tool development, and Docker optimization.
1. Go Fundamentals: Types, Structs, Methods, Pointers
1.1 Basic Types and Zero Values
Every variable in Go is initialized to its zero value upon declaration:
package main
import "fmt"
func main() {
// Basic types and zero values
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (empty string)
// Short declaration (type inference)
name := "gopher" // string
age := 10 // int
pi := 3.14 // float64
active := true // bool
// Constants
const MaxRetries = 3
const (
StatusOK = 200
StatusError = 500
)
fmt.Println(i, f, b, s, name, age, pi, active)
}
1.2 Slices vs Arrays
Arrays have fixed size; slices are dynamic-size views over arrays:
// Array: size is part of the type
var arr [5]int // [0, 0, 0, 0, 0]
matrix := [2][3]int{{1,2,3}, {4,5,6}}
// Slice: dynamic, used 99% in practice
slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // [1, 2, 3, 4, 5]
// Create with make (length, capacity)
buf := make([]byte, 0, 1024) // length 0, capacity 1024
// Slicing (shares underlying array!)
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3] - view of original
sub[0] = 99 // original also changes: [1, 99, 3, 4, 5]
// Safe copy
copied := make([]int, len(original))
copy(copied, original)
1.3 Maps
// Create map
m := map[string]int{
"apple": 5,
"banana": 3,
}
// Check existence (comma ok pattern)
val, ok := m["cherry"]
if !ok {
fmt.Println("cherry not found")
}
// Iteration (order not guaranteed)
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// Delete
delete(m, "apple")
1.4 Structs and Methods
type User struct {
ID int
Name string
Email string
CreatedAt time.Time
}
// Value receiver method (read-only)
func (u User) FullName() string {
return u.Name
}
// Pointer receiver method (can modify)
func (u *User) UpdateEmail(email string) {
u.Email = email
}
// Constructor pattern (Go has no constructors, use functions)
func NewUser(name, email string) *User {
return &User{
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
1.5 Pointers
Go pointers are safe — unlike C, pointer arithmetic is not allowed:
func main() {
x := 42
p := &x // get address
fmt.Println(*p) // dereference: 42
*p = 100
fmt.Println(x) // 100
// nil pointer check
var ptr *int
if ptr != nil {
fmt.Println(*ptr)
}
}
// Value passing vs pointer passing
func doubleValue(n int) { n *= 2 } // original unchanged
func doublePointer(n *int) { *n *= 2 } // original modified
2. Interfaces and Composition
2.1 Implicit Interfaces (Duck Typing)
Go interfaces unlike Java/C# require no explicit declaration. Implementing the methods automatically satisfies the interface:
// Interface definition
type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}
// Interface composition
type ReadWriter interface {
Reader
Writer
}
// MyBuffer satisfies Writer without explicit declaration
type MyBuffer struct {
data []byte
}
func (b *MyBuffer) Write(p []byte) (int, error) {
b.data = append(b.data, p...)
return len(p), nil
}
// Can pass MyBuffer to any function accepting Writer
func SaveToWriter(w Writer, content string) error {
_, err := w.Write([]byte(content))
return err
}
2.2 The Power of Small Interfaces
The Go standard library's core philosophy is small interfaces:
Go Standard Library Core Interfaces:
┌──────────────────────────────────────────────────┐
│ io.Reader - Read(p []byte) (n, err) │
│ io.Writer - Write(p []byte) (n, err) │
│ io.Closer - Close() error │
│ fmt.Stringer - String() string │
│ error - Error() string │
│ sort.Interface - Len, Less, Swap │
│ http.Handler - ServeHTTP(w, r) │
│ json.Marshaler - MarshalJSON() ([]byte, err) │
└──────────────────────────────────────────────────┘
// Practical example: process any Reader
func CountLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
// Works with files, HTTP responses, strings, etc.
lines1, _ := CountLines(os.Stdin)
lines2, _ := CountLines(resp.Body)
lines3, _ := CountLines(strings.NewReader("hello\nworld"))
2.3 Struct Embedding (Composition)
Go uses embedding instead of inheritance for code reuse:
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
// Dog embeds Animal (composition, not inheritance)
type Dog struct {
Animal // embedding
Breed string
}
func main() {
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Labrador",
}
fmt.Println(d.Speak()) // Animal's method promoted
fmt.Println(d.Name) // Direct access to Animal's field
}
3. Concurrency: Goroutines and Channels
3.1 Goroutines
Goroutines are lightweight threads managed by the Go runtime. While OS threads use about 1MB stack, goroutines start at about 2KB:
func main() {
// Start goroutine: just add the go keyword
go func() {
fmt.Println("Hello from goroutine!")
}()
// 1000 goroutines running concurrently
for i := 0; i < 1000; i++ {
go func(id int) {
fmt.Printf("Worker %d\n", id)
}(i) // pass i as argument (beware closure variable capture)
}
time.Sleep(time.Second) // wait for main goroutine
}
3.2 Channels
Channels are pipes for safely passing data between goroutines:
func main() {
// Unbuffered channel (synchronous)
ch := make(chan string)
go func() {
ch <- "hello" // send (blocks until receiver arrives)
}()
msg := <-ch // receive (blocks until sender arrives)
fmt.Println(msg)
// Buffered channel (asynchronous)
buffered := make(chan int, 3)
buffered <- 1 // no block (buffer has space)
buffered <- 2
buffered <- 3
// buffered <- 4 // blocks here (buffer full)
}
// Producer-consumer pattern
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // no more data to send
}
func consumer(ch <-chan int) {
for val := range ch { // receive until closed
fmt.Println(val)
}
}
3.3 Select Statement
Select waits on multiple channel operations simultaneously:
func main() {
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"
}()
// Receive from whichever is ready first
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(3 * time.Second):
fmt.Println("timeout!")
}
}
3.4 sync Package
// WaitGroup: wait for multiple goroutines to finish
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // wait until all goroutines complete
}
// Mutex: protect shared resources
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// Once: execute only once
var once sync.Once
var instance *Database
func GetDB() *Database {
once.Do(func() {
instance = connectDB()
})
return instance
}
3.5 context.Context
Context manages goroutine lifetimes, propagating cancellation signals and timeouts:
func fetchURL(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
return string(body), err
}
func main() {
// 3-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := fetchURL(ctx, "https://api.example.com/data")
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Request timed out!")
return
}
fmt.Println(result)
}
4. Concurrency Patterns
4.1 Fan-Out / Fan-In
Multiple goroutines distribute work (Fan-Out) and merge results into one (Fan-In):
func fanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = process(input)
}
return channels
}
func process(input <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range input {
out <- n * n // square operation
}
}()
return out
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
4.2 Worker Pool
func workerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobs {
result := processJob(job)
results <- result
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
}
4.3 Pipeline
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func filter(in <-chan int, predicate func(int) bool) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if predicate(n) {
out <- n
}
}
}()
return out
}
// Usage: compose pipeline
func main() {
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 })
for result := range evens {
fmt.Println(result) // 4, 16, 36, 64, 100
}
}
4.4 Rate Limiter and Semaphore
// Rate Limiter
func rateLimitedFetch(urls []string, rps int) {
ticker := time.NewTicker(time.Second / time.Duration(rps))
defer ticker.Stop()
for _, url := range urls {
<-ticker.C // execute only rps times per second
go fetch(url)
}
}
// Semaphore pattern (limit concurrent execution)
func semaphorePattern(tasks []Task, maxConcurrent int) {
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
sem <- struct{}{} // acquire slot
go func(t Task) {
defer wg.Done()
defer func() { <-sem }() // release slot
process(t)
}(task)
}
wg.Wait()
}
4.5 errgroup Pattern
import "golang.org/x/sync/errgroup"
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // capture loop variables
g.Go(func() error {
body, err := fetchURL(ctx, url)
if err != nil {
return fmt.Errorf("fetching %s: %w", url, err)
}
results[i] = body
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // returns first error, cancels rest
}
return results, nil
}
5. Error Handling
5.1 Basic Error Patterns
Go returns errors as values instead of using exceptions:
// Basic error return
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Add context with fmt.Errorf
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config %s: %w", path, err)
}
// ...
return config, nil
}
5.2 Sentinel Errors and Custom Errors
// Sentinel errors (package-level variables)
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
// Using errors.Is / errors.As
func handleError(err error) {
// Sentinel error comparison
if errors.Is(err, ErrNotFound) {
// handle 404
}
// Extract custom error type
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
}
}
5.3 Defer, Panic, Recover
// defer: execute in reverse order at function exit (key for resource cleanup)
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // always executes when function returns
data, err := io.ReadAll(f)
return string(data), err
}
// panic/recover: use only for truly unrecoverable situations
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
fn()
return nil
}
6. Generics (Go 1.18+)
6.1 Basic Syntax
// Generic function
func Map[T any, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// Usage
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
6.2 Type Constraints
// Custom constraint
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// comparable constraint (types usable as map keys)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// 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
}
6.3 When to Use Generics
Generics Decision Guide:
┌─────────────────────────────────────────────────────────┐
│ Good Use Cases │ Avoid When │
│ ────────────────────────────── │ ────────────────────── │
│ Collection utilities (Map) │ Only 2-3 types needed │
│ Data structures (Stack, Queue) │ Interfaces suffice │
│ Type-safe Result/Option types │ Readability suffers │
│ Sorting, searching algorithms │ Reflection needed │
└─────────────────────────────────────────────────────────┘
7. Testing
7.1 Table-Driven Tests
The idiomatic Go testing pattern:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -1, 5, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
7.2 HTTP Tests
func TestGetUserHandler(t *testing.T) {
handler := http.HandlerFunc(GetUserHandler)
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var user User
err := json.NewDecoder(rec.Body).Decode(&user)
require.NoError(t, err)
assert.Equal(t, "123", user.ID)
}
7.3 Benchmarks and Fuzzing
// Benchmark
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// Fuzzing (Go 1.18+)
func FuzzParseJSON(f *testing.F) {
f.Add([]byte(`{"name": "test"}`))
f.Add([]byte(`{}`))
f.Fuzz(func(t *testing.T, data []byte) {
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return
}
_, err := json.Marshal(result)
if err != nil {
t.Errorf("re-marshal failed: %v", err)
}
})
}
# Running tests
go test ./... # all tests
go test -v -run TestUser ./pkg/user # specific test
go test -bench=. ./... # benchmarks
go test -fuzz=FuzzParseJSON ./... # fuzzing
go test -race ./... # race detector
go test -cover ./... # coverage
8. Building REST APIs
8.1 Chi Router + Middleware
package main
import (
"encoding/json"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Server struct {
router *chi.Mux
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
s := &Server{
router: chi.NewRouter(),
db: db,
}
s.routes()
return s
}
func (s *Server) routes() {
s.router.Use(middleware.Logger)
s.router.Use(middleware.Recoverer)
s.router.Use(middleware.Timeout(30 * time.Second))
s.router.Route("/api/v1", func(r chi.Router) {
r.Get("/health", s.handleHealth)
r.Route("/users", func(r chi.Router) {
r.Get("/", s.handleListUsers)
r.Post("/", s.handleCreateUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", s.handleGetUser)
r.Put("/", s.handleUpdateUser)
r.Delete("/", s.handleDeleteUser)
})
})
})
}
func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
var input CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
user, err := s.userService.Create(r.Context(), input)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusCreated, user)
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
9. Building gRPC Services
9.1 Protocol Buffers Definition
syntax = "proto3";
package user.v1;
option go_package = "gen/user/v1;userv1";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc StreamUpdates(StreamUpdatesRequest) returns (stream UserEvent);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
9.2 gRPC Server Implementation
type userServer struct {
userv1.UnimplementedUserServiceServer
repo UserRepository
}
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
user, err := s.repo.GetByID(ctx, req.Id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "internal error")
}
return &userv1.GetUserResponse{User: toProtoUser(user)}, nil
}
// Server streaming
func (s *userServer) StreamUpdates(req *userv1.StreamUpdatesRequest, stream userv1.UserService_StreamUpdatesServer) error {
for event := range s.eventCh {
if err := stream.Send(event); err != nil {
return err
}
}
return nil
}
// Interceptor (middleware)
func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s error=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
10. CLI Tool Development
10.1 Cobra + Viper
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "A powerful CLI tool",
Long: "mytool is a CLI application for managing deployments",
}
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
env := args[0]
image, _ := cmd.Flags().GetString("image")
replicas, _ := cmd.Flags().GetInt("replicas")
dryRun, _ := cmd.Flags().GetBool("dry-run")
fmt.Printf("Deploying to %s: image=%s, replicas=%d, dry-run=%v\n",
env, image, replicas, dryRun)
if dryRun {
fmt.Println("Dry run mode - no changes applied")
return nil
}
return executeDeploy(env, image, replicas)
},
}
func init() {
deployCmd.Flags().StringP("image", "i", "", "Container image (required)")
deployCmd.Flags().IntP("replicas", "r", 1, "Number of replicas")
deployCmd.Flags().Bool("dry-run", false, "Dry run mode")
deployCmd.MarkFlagRequired("image")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
rootCmd.AddCommand(deployCmd)
}
11. Production Deployment
11.1 Docker Multi-Stage Build (5MB Binary)
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /app/server ./cmd/server
# Run stage (scratch = empty image)
FROM scratch
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# Build result size comparison
# Go scratch: ~5-10MB
# Go alpine: ~15MB
# Node.js: ~200MB+
# Java Spring: ~300MB+
# Python: ~150MB+
11.2 Graceful Shutdown
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
log.Printf("Server starting on :8080")
if err := srv.ListenAndServe(); 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 gracefully...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Forced shutdown: %v", err)
}
log.Println("Server stopped")
}
11.3 pprof Profiling
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe(":6060", nil))
}()
// main server ...
}
# CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Memory profile
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutine profile
go tool pprof http://localhost:6060/debug/pprof/goroutine
12. Go vs Rust vs Java Comparison
┌─────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ Category │ Go │ Rust │ Java │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Learning Curve │ Low (1-2 weeks) │ High (3-6 months)│ Medium (1-2 mo) │
│ Compile Speed │ Very fast │ Slow │ Moderate │
│ Runtime Speed │ Fast │ Very fast │ Fast (JIT) │
│ Memory Mgmt │ GC │ Ownership system │ GC │
│ Concurrency │ Goroutines/Chan │ async/tokio │ Virtual Threads │
│ Binary Size │ 5-15MB │ 1-5MB │ 100MB+ (JRE) │
│ Ecosystem │ Cloud/Infra │ Systems/WASM │ Enterprise │
│ Error Handling │ Value return │ Result/Option │ Exceptions │
│ Generics │ 1.18+ (basic) │ Powerful │ Full support │
│ Primary Use │ Microservices, │ OS, game engines,│ Large enterprise,│
│ │ CLI, DevOps │ embedded │ Android │
│ Adoption │ Google, Uber │ Mozilla, AWS │ Most enterprises │
│ Salary (US) │ High (top 5) │ Very high (#1) │ Average │
└─────────────────┴──────────────────┴──────────────────┴──────────────────┘
Go excels at: Microservices, API servers, CLI tools, DevOps tooling, network programming Rust excels at: Systems programming, game engines, embedded, maximum performance needs Java excels at: Large enterprise, Android apps, legacy system maintenance
13. Interview Questions (15)
Fundamentals
-
What is the difference between slices and arrays in Go? Arrays have fixed size as part of the type (e.g.,
[5]int). Slices are dynamic-sized views over arrays, internally holding a pointer, length, and capacity. -
Why does Go not have inheritance? How do you reuse code? Go uses struct embedding (composition) instead of inheritance. It enforces the "composition over inheritance" principle at the language level.
-
What is the difference between a nil interface and an interface with a nil pointer? An interface is a (type, value) pair. Both nil means nil interface. Having a type but nil value means non-nil interface.
-
What is the execution order of defer, and what are the gotchas? LIFO (last in, first out). Arguments are captured at defer time, so be careful with defer in loops.
-
Explain Go's zero value philosophy. Every variable gets a valid default value on declaration, preventing uninitialized variable bugs. This reduces the need for constructors.
Concurrency
-
Why are goroutines lighter than OS threads? Goroutines start with ~2KB stack (OS threads use 1MB+), and Go's M:N scheduler maps thousands of goroutines to few OS threads.
-
Buffered vs unbuffered channels? Unbuffered channels require sender and receiver to meet synchronously. Buffered channels allow asynchronous sends up to capacity.
-
When do goroutine leaks happen and how to prevent them? When there is no counterpart to send/receive on a channel, or when context cancellation is not handled. Using context.WithCancel/Timeout is key.
-
What are deadlock conditions and how does Go detect them? Go runtime detects when all goroutines are blocked and panics. go vet and the race detector also help.
-
When to use sync.Mutex vs sync.RWMutex? RWMutex is better when reads vastly outnumber writes, as multiple goroutines can read simultaneously.
Advanced
-
What are the 3 main uses of context.Context? Cancellation propagation (Cancel), timeouts (Timeout/Deadline), value passing (WithValue). Should always be used with HTTP handlers and DB calls.
-
What are the characteristics of Go's GC? Go uses a concurrent mark-and-sweep GC with sub-millisecond STW pauses. Tunable via GOGC environment variable.
-
Explain code differences before and after generics were introduced. Before: required interface with type assertions or code generation. After: type parameters provide compile-time type safety.
-
How do you do dependency injection in Go? Typically through constructor functions that accept interfaces. DI frameworks like wire (Google) and fx (Uber) are also available.
-
How does Go module handle indirect dependencies? The indirect comment in go.mod marks dependencies not directly imported. Use
go mod tidyto clean up andgo mod vendorfor vendoring.
14. Practice Quiz
Q1: What is the output of this code?
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}
Answer: Before Go 1.22, mostly prints 3, 3, 3. The closure references loop variable i, which is already 3 when goroutines execute. From Go 1.22, loop variables are created fresh each iteration, outputting 0, 1, 2 (order undefined).
Q2: Why does this code deadlock?
func main() {
ch := make(chan int)
ch <- 42
fmt.Println(<-ch)
}
Answer: Sending to an unbuffered channel blocks until a receiver arrives. The main goroutine blocks at ch <- 42 and can never reach <-ch, causing deadlock. Fix: send in a separate goroutine or use a buffered channel.
Q3: Nil interface problem - why is err != nil true?
type MyError struct{}
func (e *MyError) Error() string { return "error" }
func doSomething() error {
var err *MyError = nil
return err
}
func main() {
err := doSomething()
fmt.Println(err == nil) // false!
}
Answer: doSomething() returns an interface with (type=*MyError, value=nil). Since the interface has a type, it is not nil. Fix: explicitly return nil from the function.
Q4: Common WaitGroup mistake?
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
go func(id int) {
wg.Add(1) // wrong position!
defer wg.Done()
fmt.Println(id)
}(i)
}
wg.Wait()
}
Answer: wg.Add(1) is inside the goroutine, so wg.Wait() may execute before any Add. The correct approach is to call wg.Add(1) before starting the goroutine.
Q5: Explain how context cancellation propagates.
Answer: context.WithCancel creates a child context that monitors the parent's Done channel. When the parent is cancelled, the child's Done channel also closes. This chain propagates to the top, allowing a single cancellation to clean up an entire request tree. Goroutines should check ctx.Done() in select statements to terminate properly.
References
- Go Official Documentation
- Effective Go
- Go by Example
- Go Concurrency Patterns (Rob Pike)
- Advanced Go Concurrency Patterns
- The Go Programming Language (Donovan & Kernighan)
- Concurrency in Go (Katherine Cox-Buday)
- Go Wiki: Table Driven Tests
- Go Wiki: Code Review Comments
- Uber Go Style Guide
- Go Module Reference
- Go Generics Tutorial
- pprof Guide
- Chi Router
- gRPC Go Official Guide
- Cobra CLI Library
- BubbleTea TUI Framework