The Ultimate GoLang Cheat Sheet (Go 1.21+)
The Ultimate GoLang Cheat Sheet (Go 1.21+), We cover the basics and some "not so basic" concepts in an easy to digest cheat sheet. For all Go versions 1.21 and above.
Go is the language that’s both minimal and powerful — a rare combination that has made it a favorite for backend, distributed systems, and DevOps work. It’s easy to learn but deep enough to handle production-grade workloads at massive scale.
With Go 1.21+, we have new tools like slices
and maps
helpers, profile-guided optimization (PGO), and small but meaningful language refinements. This cheat sheet is designed to be both quick reference and learning resource — covering everything from syntax basics to concurrency patterns, common idioms, and modern Go 1.21+ features.
1. Basics & Syntax
Hello, World
package main
import "fmt"
func main() {
fmt.Println("Hello, Go 1.21+!")
}
This is Go’s simplest program. Every executable program starts with package main
— it’s how Go knows to compile it as a binary rather than a library.
The main()
function is the entry point. Go begins execution there. fmt.Println
comes from Go’s standard formatting package, which provides functions for printing and string formatting. Even though this example is simple, the same structure underpins all Go programs — whether they’re tiny command-line tools or large microservices.
Variables & Constants
// Explicit type
var name string = "Kirk"
// Type inference
age := 36
// Constants
const pi = 3.14159
Go supports both explicit type declarations and type inference. If you know the exact type and want to be explicit, you use the var
keyword followed by the type. If you prefer brevity, :=
lets Go infer the type for you.
Constants (const
) are immutable and must be set at compile time. They’re great for values like configuration keys, fixed mathematical constants, or application limits. Using constants where possible makes code safer and easier to maintain.
Multiple Return Values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
Multiple return values are one of Go’s most important features. This pattern — returning a result and an error
— is the standard way Go handles error checking instead of exceptions.
In this example, divide
either returns the division result or an error if the divisor is zero. The nil
value is used to signal “no error.” This explicit approach keeps error handling visible and forces the developer to deal with failures immediately.
2. Control Structures
if x > 10 {
fmt.Println("Big number")
} else {
fmt.Println("Small number")
}
switch day := "Tuesday"; day {
case "Monday":
fmt.Println("Start of week")
case "Tuesday":
fmt.Println("Still early")
default:
fmt.Println("Weekend soon")
}
The if
statement in Go works as expected but also supports an optional short variable declaration (if x := expr; condition
).
The switch
statement is more flexible than in many languages. It automatically breaks after each case (no need for break
), supports multiple comma-separated values in a single case
, and can match on arbitrary expressions, not just constants.
For Loops
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// While-style loop
i := 0
for i < 5 {
fmt.Println(i)
i++
}
// Infinite loop
for {
fmt.Println("Forever")
break
}
Go’s only loop keyword is for
, but it covers the roles of both for
and while
in other languages.
The first example is the classic counter loop, the second behaves like a while loop, and the third is an infinite loop where you manually break. This single keyword approach keeps the language simpler without sacrificing capability.
3. Collections & Generics
Slices
nums := []int{1, 2, 3}
nums = append(nums, 4, 5)
for i, n := range nums {
fmt.Printf("%d: %d\n", i, n)
}
Slices are Go’s resizable arrays. Unlike arrays, their size isn’t fixed. The append
function automatically handles capacity management.
The range
keyword allows iteration over both index and value. Slices are used in the majority of Go programs because they are memory-efficient, safe, and flexible.
Maps
ages := map[string]int{"Kirk": 36, "Bob": 29}
ages["Alice"] = 41
for k, v := range ages {
fmt.Printf("%s is %d years old\n", k, v)
}
Maps in Go are hash tables that provide constant-time lookups. You can add and update keys directly using the map[key] = value
syntax.
Iteration order in maps is random — a deliberate design choice to prevent reliance on predictable ordering, which can cause subtle bugs in distributed systems.
New Go 1.21 slices
& maps
Helpers
import (
"fmt"
"slices"
"maps"
)
func main() {
s := []int{3, 1, 2}
slices.Sort(s) // [1, 2, 3]
m1 := map[string]int{"a": 1}
m2 := map[string]int{"b": 2}
maps.Copy(m1, m2) // merge
}
Go 1.21 added slices
and maps
helper packages that eliminate boilerplate. Before these, sorting required importing sort
and writing custom functions.
Now you can sort, search, delete elements, or merge maps directly with concise, type-safe standard library functions.
4. Functions & Closures
func adder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func main() {
add5 := adder(5)
fmt.Println(add5(3)) // 8
}
Closures in Go are functions that capture variables from their surrounding scope. In this example, the adder
function returns another function that adds a fixed value x
to whatever argument you pass later.
This pattern is useful for building small, composable pieces of behavior without needing a full struct or interface.
5. Structs & Methods
type User struct {
Name string
Age int
}
func (u User) Greet() {
fmt.Printf("Hi, I'm %s\n", u.Name)
}
func (u *User) Birthday() {
u.Age++
}
Structs in Go group related fields into a single type. Methods can be defined with either value receivers or pointer receivers.
Value receivers get a copy of the struct, which is fine for read-only actions like Greet
. Pointer receivers modify the original data, as in the Birthday
method.
6. Interfaces
type Greeter interface {
Greet()
}
func SayHello(g Greeter) {
g.Greet()
}
type Person struct{ Name string }
func (p Person) Greet() { fmt.Println("Hello,", p.Name) }
func main() {
p := Person{"Kirk"}
SayHello(p)
}
Interfaces in Go are implicitly satisfied — if a type implements all the methods required by an interface, it automatically satisfies it without explicit declaration.
In this example, Greeter
requires a Greet()
method. Person
implements Greet()
, so we can pass it to SayHello
without writing extra boilerplate. This design promotes loose coupling and allows you to swap out implementations easily in testing or production.
7. Concurrency Patterns
Goroutines & Channels
ch := make(chan int)
go func() {
ch <- 42
}()
val := <-ch
fmt.Println(val)
Goroutines are lightweight threads managed by the Go runtime. You create one with the go
keyword, and it runs concurrently with the rest of your program.
Channels provide safe communication between goroutines. In this example, one goroutine sends a value to ch
, and the main goroutine receives it. This pattern avoids shared-memory concurrency issues by encouraging message passing.
Buffered Channels
ch := make(chan string, 2)
ch <- "Hello"
ch <- "World"
fmt.Println(<-ch)
Buffered channels have capacity, allowing you to send values without an immediate receiver — up to the buffer limit.
They’re useful when you need to handle bursts of work without blocking immediately, such as when implementing worker queues or rate-limited pipelines.
Worker Pool
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}
A worker pool is a classic concurrency pattern where multiple goroutines process jobs from a shared channel.
Here, three workers process integers and return results. Worker pools are efficient for CPU-bound tasks and prevent spawning too many goroutines, which can waste resources or overwhelm external systems.
8. Error Handling
Basic Error Check
if err := doThing(); err != nil {
log.Fatal(err)
}
Go uses explicit error checking instead of exceptions. This keeps error handling visible and predictable.
The if err := ...; err != nil
pattern is idiomatic and keeps the variable scoped only where needed. Using log.Fatal
immediately stops execution after logging the error.
Custom Error
type NotFoundError struct{ Item string }
func (e NotFoundError) Error() string {
return fmt.Sprintf("%s not found", e.Item)
}
Custom error types allow richer error handling. By implementing the Error()
method, your type satisfies Go’s error
interface.
This makes it easy to differentiate between error types using type assertions or errors.As
, enabling more precise recovery or retry logic.
9. Contexts
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil)
res, err := http.DefaultClient.Do(req)
The context
package is Go’s way to propagate deadlines, cancellation signals, and request-scoped values across goroutines.
In this example, the HTTP request is automatically canceled if it takes longer than one second. This is essential for preventing goroutine leaks and controlling resource usage in distributed systems.
10. Testing
func Add(a, b int) int { return a + b }
func TestAdd(t *testing.T) {
got := Add(2, 2)
want := 4
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
Go’s built-in testing
package makes unit tests simple. A test function must start with Test
and take a *testing.T
argument.
You can run tests with go test ./...
. The testing framework integrates seamlessly with benchmarks and examples, encouraging developers to keep tests close to the code.
11. File I/O
data, err := os.ReadFile("file.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
err = os.WriteFile("out.txt", []byte("Hello"), 0644)
Reading and writing files is straightforward with os.ReadFile
and os.WriteFile
.
The 0644
permission mode means owner can read/write, others can only read. Go’s error-first style ensures you handle file access issues (permissions, missing files, etc.) immediately.
12. Common Patterns
Singleton
var (
instance *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
instance = &Config{}
})
return instance
}
The singleton pattern ensures only one instance of a type exists in your application.
sync.Once
guarantees the initialization code runs exactly once, even in the presence of multiple goroutines, avoiding race conditions.
Graceful Shutdown
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
Graceful shutdown ensures your application cleans up resources before exiting.
Here, we listen for an interrupt signal, give the server up to five seconds to finish processing, and then shut it down. This prevents abrupt connection drops and data loss.
Rate Limiting
limiter := time.Tick(200 * time.Millisecond)
for req := range requests {
<-limiter
go handle(req)
}
Rate limiting is crucial for avoiding API overuse or server overload.
time.Tick
returns a channel that delivers “ticks” at regular intervals. Each request waits for a tick before being processed, ensuring a steady flow rather than bursts.
13. Tooling & Tips
- Benchmarking:
Linting:
go vet ./...
golangci-lint run
Finds suspicious constructs and potential bugs.
Format code:
go fmt ./...
Keeps your code style consistent automatically.
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
Run with go test -bench=.
, and Go will execute your benchmark function repeatedly to measure performance.
Dependency management:
go mod init example.com/myapp
go mod tidy
Keeps your module dependencies up to date.
14. Go 1.21+ Highlights
slices
package — simplifies sorting, searching, and element deletion without external packages.maps
package — adds helpers for merging, cloning, and comparing maps.- Profile-guided optimization (PGO) — lets you tune builds using actual runtime profiles for better performance.
- Improved type inference with generics — cleaner, less verbose generic functions.
These features keep Go competitive while retaining its minimalist philosophy.
Final Thoughts
Go’s philosophy is to provide just enough language features to build scalable, maintainable, and performant systems without unnecessary complexity.
With Go 1.21+, the standard library has taken another step toward making everyday development more ergonomic without losing the simplicity that made Go famous. Bookmark this guide — it’s the perfect reference for when you need a quick refresh on syntax, patterns, or best practices.