Go (Golang): The Complete Guide for 2026
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
- Why Go? Use Cases and Strengths
- Installing Go and Workspace Setup
- Variables, Types, and Constants
- Functions and Multiple Returns
- Structs and Methods
- Interfaces and Polymorphism
- Pointers
- Arrays, Slices, and Maps
- Control Flow
- Error Handling Patterns
- Goroutines and Channels
- The sync Package
- Packages and Modules
- Testing with go test
- Building HTTP Servers
- Working with JSON
- The Context Package
- Go Tooling
- Best Practices and Idioms
- 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.
- Cloud infrastructure — Docker, Kubernetes, Terraform, Consul, Vault
- Web APIs and microservices — net/http is production-ready out of the box
- CLI tools — single binary distribution, fast startup, cross-compilation
- DevOps tooling — Prometheus, Grafana agent, Loki, Jaeger
- Fast compilation — large projects compile in seconds, not minutes
- Built-in concurrency — goroutines and channels make concurrent code simple
- Single binary — no runtime, no dependencies, just copy and run
- Cross-compilation — build for any OS/architecture from any platform
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)
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
- REST API Design: The Complete Guide — design patterns for APIs you build with Go
- Docker: The Complete Guide — containerize Go services with minimal images
- Kubernetes: The Complete Guide — orchestrate Go microservices at scale
- GraphQL: The Complete Guide — build GraphQL APIs in Go with gqlgen