Go (Golang): The Complete Guide for 2026

Published February 12, 2026 · 35 min read

Go is the language behind the infrastructure that runs the modern internet. Docker, Kubernetes, Terraform, Prometheus, etcd, CockroachDB — all written in Go. If you build backend services, CLI tools, or cloud-native software, Go is not just worth learning — it is essential.

This guide takes you from zero to productive Go developer. Every section includes practical code you can run immediately, covering fundamentals through concurrency, HTTP servers, testing, and production best practices.

Table of Contents

  1. Why Go? Use Cases and Strengths
  2. Installing Go and Workspace Setup
  3. Variables, Types, and Constants
  4. Functions and Multiple Returns
  5. Structs and Methods
  6. Interfaces and Polymorphism
  7. Pointers
  8. Arrays, Slices, and Maps
  9. Control Flow
  10. Error Handling Patterns
  11. Goroutines and Channels
  12. The sync Package
  13. Packages and Modules
  14. Testing with go test
  15. Building HTTP Servers
  16. Working with JSON
  17. The Context Package
  18. Go Tooling
  19. Best Practices and Idioms
  20. FAQ

1. Why Go? Use Cases and Strengths

Go was created at Google in 2009 by Robert Griesemer, Rob Pike, and Ken Thompson to solve real problems: slow compilation in C++, dependency management nightmares, and the difficulty of writing concurrent software.

2. Installing Go and Workspace Setup

# Download and install (Linux/macOS)
wget https://go.dev/dl/go1.23.6.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.23.6.linux-amd64.tar.gz

# Add to PATH (~/.bashrc or ~/.zshrc)
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin

# Verify and create your first project
go version               # go version go1.23.6 linux/amd64
mkdir myproject && cd myproject
go mod init github.com/yourname/myproject
// main.go — your first Go program
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go!")
}
// Run: go run main.go
// Build: go build -o myapp main.go

3. Variables, Types, and Constants

// Explicit declaration
var name string = "Alice"
var age int = 30

// Short declaration with type inference (most common)
city := "Berlin"       // string
score := 98.5          // float64
count := 42            // int
active := true         // bool

// Zero values: int=0, float64=0.0, string="", bool=false, pointer=nil

// Multiple declarations
var (
    width  = 100
    height = 200
)

// Constants and iota
const Pi = 3.14159
const (
    StatusPending  = iota // 0
    StatusActive          // 1
    StatusInactive        // 2
)

// Basic types: bool, string, int, int8/16/32/64, uint,
// float32/64, byte (uint8), rune (int32 — Unicode code point)

4. Functions and Multiple Returns

// Basic function
func add(a, b int) int { return a + b }

// Multiple return values (idiomatic Go)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

// Named return values
func parseConfig(path string) (host string, port int, err error) {
    host = "localhost"
    port = 8080
    return  // bare return uses named values
}

// Variadic function
func sum(nums ...int) int {
    total := 0
    for _, n := range nums { total += n }
    return total
}

// Closures
func counter() func() int {
    count := 0
    return func() int { count++; return count }
}

// Usage
result, err := divide(10, 3)
if err != nil { log.Fatal(err) }
fmt.Println(sum(1, 2, 3, 4))  // 10
next := counter()
fmt.Println(next(), next())     // 1 2

5. Structs and Methods

type User struct {
    ID    int
    Name  string
    Email string
    Active bool
}

// Value receiver (does not modify original)
func (u User) FullInfo() string {
    return fmt.Sprintf("%s (%s)", u.Name, u.Email)
}

// Pointer receiver (can modify original)
func (u *User) Deactivate() { u.Active = false }

// Constructor (convention, not a language feature)
func NewUser(name, email string) *User {
    return &User{Name: name, Email: email, Active: true}
}

// Embedding (composition over inheritance)
type Admin struct {
    User              // Embedded — Admin "has a" User
    Permissions []string
}

admin := Admin{
    User: User{Name: "Carol", Email: "carol@example.com"},
    Permissions: []string{"read", "write", "admin"},
}
fmt.Println(admin.Name)       // Promoted from User
fmt.Println(admin.FullInfo()) // Promoted method

6. Interfaces and Polymorphism

Go interfaces are satisfied implicitly — no implements keyword. A type implements an interface by implementing its methods.

type Storage interface {
    Save(key string, value []byte) error
    Load(key string) ([]byte, error)
}

type MemoryStorage struct { data map[string][]byte }

func (m *MemoryStorage) Save(key string, value []byte) error {
    m.data[key] = value
    return nil
}
func (m *MemoryStorage) Load(key string) ([]byte, error) {
    v, ok := m.data[key]
    if !ok { return nil, fmt.Errorf("key not found: %s", key) }
    return v, nil
}

// Any function accepting Storage works with MemoryStorage,
// FileStorage, S3Storage, RedisStorage — no coupling
func SaveData(store Storage, key string, data []byte) error {
    return store.Save(key, data)
}

// The empty interface (any) accepts any type
func printValue(v any) { fmt.Printf("Type: %T, Value: %v\n", v, v) }

// Type switch
switch v := value.(type) {
case string:  fmt.Println("string:", v)
case int:     fmt.Println("int:", v)
default:      fmt.Println("unknown:", v)
}

// Key standard library interfaces:
// io.Reader:   Read(p []byte) (n int, err error)
// io.Writer:   Write(p []byte) (n int, err error)
// fmt.Stringer: String() string
// error:       Error() string

7. Pointers

x := 42
p := &x        // p is *int (pointer to x)
fmt.Println(*p) // 42 (dereference)
*p = 100
fmt.Println(x)  // 100 (modified through pointer)

// Pass by value vs pointer
func doubleValue(n int)   { n = n * 2 }    // Local copy only
func doublePtr(n *int)    { *n = *n * 2 }  // Modifies original

n := 10
doubleValue(n); fmt.Println(n)  // 10 (unchanged)
doublePtr(&n);  fmt.Println(n)  // 20 (modified)

// When to use pointers:
// - Need to modify the argument
// - Large structs (avoid copying)
// - Pointer receivers when methods modify the struct
// Go has no pointer arithmetic (unlike C)

8. Arrays, Slices, and Maps

// Slices (dynamic — the workhorse collection)
nums := []int{1, 2, 3, 4, 5}
names := make([]string, 0, 10)    // length 0, capacity 10
nums = append(nums, 6, 7)         // Append elements
sub := nums[1:4]                   // [2, 3, 4]

for i, v := range nums {
    fmt.Printf("index %d: %d\n", i, v)
}

// Maps (key-value pairs)
ages := map[string]int{"Alice": 30, "Bob": 25}
ages["Carol"] = 28                 // Add
delete(ages, "Bob")                // Delete
age, ok := ages["Alice"]          // Check existence
if !ok { fmt.Println("not found") }

for name, age := range ages {
    fmt.Printf("%s is %d\n", name, age)
}

// Important: var m map[string]int creates a nil map
// Reads work on nil maps, but writes panic!
// Always use make() or a literal: m := map[string]int{}

9. Control Flow

// if-else (no parentheses around condition)
if x > 10 {
    fmt.Println("big")
} else if x > 5 {
    fmt.Println("medium")
} else {
    fmt.Println("small")
}

// if with init statement
if err := doSomething(); err != nil {
    log.Fatal(err)
}

// for is the ONLY loop keyword in Go
for i := 0; i < 10; i++ { fmt.Println(i) }  // Classic
for count < 100 { count++ }                   // While-style
for { break }                                  // Infinite

// Switch (no break needed, no fallthrough by default)
switch os := runtime.GOOS; os {
case "linux":  fmt.Println("Linux")
case "darwin": fmt.Println("macOS")
default:       fmt.Println(os)
}

// Defer: runs when surrounding function returns
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()  // Guaranteed cleanup

// Multiple defers execute in LIFO (stack) order

10. Error Handling Patterns

// The fundamental pattern
result, err := doSomething()
if err != nil {
    return fmt.Errorf("failed to do something: %w", err)
}

// Sentinel errors
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")

func findUser(id int) (*User, error) {
    user, ok := users[id]
    if !ok { return nil, ErrNotFound }
    return user, nil
}

// Wrapping errors with context (%w preserves the chain)
func getUser(id int) (*User, error) {
    user, err := findUser(id)
    if err != nil {
        return nil, fmt.Errorf("getUser(%d): %w", id, err)
    }
    return user, nil
}

// Checking wrapped errors
if errors.Is(err, ErrNotFound) { http.Error(w, "Not Found", 404) }

// Custom error types
type ValidationError struct { Field, Message string }
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}

var valErr *ValidationError
if errors.As(err, &valErr) { fmt.Println("Bad field:", valErr.Field) }

11. Goroutines and Channels

Goroutines are lightweight threads managed by the Go runtime (2KB stack vs 1-8MB OS threads). Channels are typed conduits for safe communication between goroutines.

// Launch goroutines with the go keyword
ch := make(chan string)
go func() { ch <- "hello from goroutine" }()
msg := <-ch  // Receive (blocks until value sent)

// Buffered channels
ch := make(chan int, 5)  // Can hold 5 values before blocking

// Fan-out: multiple goroutines processing work
func fetchAll(urls []string) []string {
    results := make(chan string, len(urls))
    for _, url := range urls {
        go func(u string) { results <- fetch(u) }(url)
    }
    var out []string
    for range urls { out = append(out, <-results) }
    return out
}

// Select: multiplex channel operations
select {
case msg := <-ch1:
    fmt.Println("From ch1:", msg)
case msg := <-ch2:
    fmt.Println("From ch2:", msg)
case <-time.After(5 * time.Second):
    fmt.Println("Timeout!")
}

// Channel direction in signatures
func producer(out chan<- int) { out <- 42; close(out) }
func consumer(in <-chan int)  { for v := range in { fmt.Println(v) } }

12. The sync Package

// WaitGroup: wait for goroutines to finish
var wg sync.WaitGroup
for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        fetch(u)
    }(url)
}
wg.Wait()

// Mutex: protect shared state
type SafeCounter struct {
    mu    sync.Mutex
    count map[string]int
}
func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count[key]++
}

// RWMutex: multiple readers, single writer
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock(); defer c.mu.RUnlock()
    v, ok := c.data[key]; return v, ok
}
func (c *Cache) Set(key, val string) {
    c.mu.Lock(); defer c.mu.Unlock()
    c.data[key] = val
}

// Once: run initialization exactly once
var once sync.Once
var db *Database
func GetDB() *Database {
    once.Do(func() { db = connectDB() })
    return db
}

13. Packages and Modules

# Module commands
go mod init github.com/yourname/project  # Initialize
go get github.com/go-chi/chi/v5          # Add dependency
go mod tidy                               # Clean up
go mod verify                             # Verify checksums
go mod vendor                             # Vendor dependencies
// Recommended project structure
myproject/
  go.mod              // Module definition + dependencies
  go.sum              // Dependency checksums (lock file)
  main.go             // Entry point
  cmd/server/main.go  // Additional entry points
  internal/           // Private packages (cannot be imported externally)
    database/db.go
    auth/auth.go
  pkg/                // Public reusable packages
    middleware/logging.go

// Visibility rule: Uppercase = exported, lowercase = unexported
type User struct {    // Exported (public)
    Name  string      // Exported field
    email string      // Unexported (private to package)
}
func NewUser() *User {} // Exported function
func validate() bool {} // Unexported function

14. Testing with go test

// math_test.go (file must end in _test.go)
package math

import "testing"

func TestAdd(t *testing.T) {
    if got := Add(2, 3); got != 5 {
        t.Errorf("Add(2, 3) = %d, want 5", got)
    }
}

// Table-driven tests (idiomatic Go)
func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b, want float64
        wantErr bool
    }{
        {"positive", 10, 2, 5, false},
        {"negative", -10, 2, -5, false},
        {"zero div", 10, 0, 0, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Fatalf("err = %v, wantErr %v", err, tt.wantErr)
            }
            if !tt.wantErr && got != tt.want {
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}

// Benchmarks
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ { Add(100, 200) }
}
# Running tests
go test ./...                  # All packages
go test -v ./...               # Verbose
go test -run TestDivide ./...  # Specific test
go test -cover ./...           # Coverage
go test -bench=. ./...         # Benchmarks
go test -race ./...            # Race detector

15. Building HTTP Servers

// Standard library server (Go 1.22+ with path params)
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

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

    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
    })

    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        json.NewEncoder(w).Encode(map[string]string{"id": id})
    })

    mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
        var user struct {
            Name  string `json:"name"`
            Email string `json:"email"`
        }
        if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
            http.Error(w, "Bad Request", 400)
            return
        }
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(user)
    })

    log.Println("Starting server on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
// With chi router (lightweight, idiomatic)
// go get github.com/go-chi/chi/v5
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

r.Route("/api/users", func(r chi.Router) {
    r.Get("/", listUsers)
    r.Post("/", createUser)
    r.Get("/{id}", getUser)
    r.Put("/{id}", updateUser)
    r.Delete("/{id}", deleteUser)
})

// Middleware pattern
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", 401); return }
        next.ServeHTTP(w, r)
    })
}

16. Working with JSON

type User struct {
    ID    int       `json:"id"`
    Name  string    `json:"name"`
    Email string    `json:"email"`
    Pass  string    `json:"-"`             // Never in JSON
    Bio   string    `json:"bio,omitempty"` // Omit if empty
}

// Marshal (struct to JSON)
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
data, _ := json.Marshal(user)
pretty, _ := json.MarshalIndent(user, "", "  ")

// Unmarshal (JSON to struct)
var u User
json.Unmarshal([]byte(jsonString), &u)

// Streaming (for HTTP handlers)
json.NewDecoder(r.Body).Decode(&user)    // Read request
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)          // Write response

// Dynamic JSON
var result map[string]any
json.Unmarshal(data, &result)
⚙ Try it: Validate and format your Go API responses with our JSON Formatter.

17. The Context Package

// Timeout context
func fetchWithTimeout(url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return nil, err }  // context.DeadlineExceeded on timeout
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

// Cancellation in long-running work
func process(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            processNext()
        }
    }
}

// Context in HTTP handlers (auto-canceled on client disconnect)
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    result, err := fetchData(ctx)
    if errors.Is(err, context.Canceled) { return }
    json.NewEncoder(w).Encode(result)
}

18. Go Tooling

# Format (the single standard style — no debates)
gofmt -w .
go fmt ./...

# Vet (find suspicious constructs)
go vet ./...

# Lint (community standard)
golangci-lint run

# Build and cross-compile
go build -o myapp .
GOOS=linux GOARCH=amd64 go build -o myapp .     # Linux
GOOS=darwin GOARCH=arm64 go build -o myapp .     # macOS ARM
GOOS=windows GOARCH=amd64 go build -o myapp.exe  # Windows

# Documentation
go doc fmt.Println
go doc -all net/http

# Profile and trace
go test -cpuprofile cpu.prof -bench .
go tool pprof cpu.prof

# Race detector
go run -race main.go

19. Best Practices and Idioms

// 1. Accept interfaces, return structs
func NewServer(store Storage) *Server { return &Server{store: store} }

// 2. Handle errors immediately
val, err := doSomething()
if err != nil { return err }  // Happy path continues unindented

// 3. Short names for short scopes
for i, v := range items { }       // Fine in loops
func (s *Server) ListUsers() { }  // Short receiver names

// 4. Keep interfaces small (1-3 methods)
type Reader interface { Read(p []byte) (n int, err error) }

// 5. Always ensure goroutines can exit
// Use context, channels, or WaitGroup — never fire and forget

// 6. Close resources with defer immediately after opening
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()

// 7. Use struct tags for serialization
type Config struct {
    Host string `json:"host" env:"APP_HOST" validate:"required"`
    Port int    `json:"port" env:"APP_PORT" validate:"min=1,max=65535"`
}

// 8. Prefer composition over inheritance (embedding)
type Server struct {
    Logger        // Embed for logging methods
    db *sql.DB
}

// 9. Use the comma-ok idiom
val, ok := myMap[key]
val, ok := myInterface.(ConcreteType)

// 10. Let gofmt and golangci-lint handle style

Frequently Asked Questions

What is Go used for and why should I learn it?

Go is a compiled, statically typed language created at Google. It is used for cloud infrastructure (Docker, Kubernetes, Terraform are all written in Go), web APIs and microservices, CLI tools, networking software, and DevOps tooling. Go compiles to a single static binary with no runtime dependencies, has built-in concurrency via goroutines and channels, produces small Docker images (often under 10MB from scratch), and has a simple syntax that an entire team can learn in weeks rather than months.

How does Go handle errors compared to try-catch in other languages?

Go does not have exceptions or try-catch blocks. Instead, functions return error values as their last return value. The caller checks if the error is nil (no error) or non-nil (error occurred) and handles it explicitly. This pattern makes error handling visible and forces developers to think about failure cases at every call site. Go provides errors.Is() and errors.As() for comparing and unwrapping errors, and fmt.Errorf with the %w verb for wrapping errors with additional context while preserving the original error chain.

What are goroutines and how are they different from threads?

Goroutines are lightweight concurrent functions managed by the Go runtime, not by the operating system. A goroutine starts with only 2KB of stack space (compared to 1-8MB for OS threads) and the Go scheduler multiplexes thousands of goroutines onto a small number of OS threads. You can launch millions of goroutines in a single program. Goroutines communicate through channels rather than shared memory, following Go's philosophy: do not communicate by sharing memory, share memory by communicating.

How do I structure a Go project with modules?

Initialize a module with go mod init github.com/user/project, which creates a go.mod file tracking your module path and dependencies. Organize code into packages by directory: cmd/ for entry points, internal/ for private packages, pkg/ for public reusable packages. Use go mod tidy to automatically add missing and remove unused dependencies. The go.sum file locks exact dependency versions for reproducible builds.

Is Go good for building REST APIs and web servers?

Go is excellent for REST APIs and web servers. The standard library's net/http package is production-ready and powers many high-traffic services without any third-party framework. Popular routers like chi and gin add minimal overhead for routing parameters and middleware. Go servers handle high concurrency efficiently thanks to goroutines, typically serving tens of thousands of concurrent connections with low memory usage. Companies like Uber, Dropbox, and Cloudflare use Go for their API infrastructure.

Conclusion

Go is the language of cloud infrastructure for a reason. It compiles fast, deploys as a single binary, handles concurrency elegantly, and has a standard library powerful enough to build production services without external dependencies. The language is deliberately simple — there is usually one obvious way to do something, which makes Go codebases readable and maintainable as teams grow.

Start with the fundamentals: write functions that return errors, use structs and interfaces, and get comfortable with goroutines and channels. Build a small HTTP server, write table-driven tests, and use golangci-lint. Everything else in the Go ecosystem builds on these foundations.

Learn More

Related Resources

REST API Design Guide
Best practices for designing RESTful APIs in Go
Docker Complete Guide
Containerize Go apps with tiny multi-stage images
Kubernetes Complete Guide
Deploy and orchestrate Go microservices at scale
JSON Formatter
Format and validate JSON from your Go API responses
GraphQL Complete Guide
Build type-safe GraphQL APIs with gqlgen for Go