Go Philosophy Basics
10 examples to get you started with Go Philosophy - 7 basic and 3 intermediate.
Prerequisites
- Go 1.26.x installed (
go version). - No third-party modules required for these snippets - all use the standard library.
Basic Examples
1. Minimal main - Clarity First
Go favors a short entry point that wires dependencies and runs.
package main
import "fmt"
func main() {
fmt.Println("ship small, readable programs")
}package mainmarks an executable; library code uses other package names.- One obvious
mainkeeps startup flow easy to audit during incidents. - Business logic belongs in packages you can test without spawning a process.
Related: Why Go Exists - the design goals behind this layout
2. Explicit Error Handling
Errors are values returned alongside results, not thrown exceptions.
package main
import (
"fmt"
"os"
)
func readConfig(path string) ([]byte, error) {
return os.ReadFile(path)
}
func main() {
data, err := readConfig("config.yaml")
if err != nil {
fmt.Fprintf(os.Stderr, "read config: %v\n", err)
os.Exit(1)
}
fmt.Printf("loaded %d bytes\n", len(data))
}- Callers handle failures at the call site - stack traces are not the default control flow.
if err != nilis repetitive by design; it keeps failure paths visible in review.- Wrapping with
fmt.Errorf("read config: %w", err)preserves causes for logs.
Related: Simplicity as a Deliberate Constraint in API Design - why APIs return plain errors
3. Small Interface at the Call Site
Go defines interfaces where they are consumed, not where types are declared.
package notify
import "context"
type Sender interface {
Send(ctx context.Context, msg string) error
}
func Deliver(ctx context.Context, s Sender, msg string) error {
return s.Send(ctx, msg)
}- Interfaces stay tiny - often one or two methods - so fakes are trivial in tests.
- Concrete types do not need to "implement" anything explicitly; satisfaction is implicit.
- This pattern favors composition and discourages deep inheritance trees.
4. Goroutine for Concurrent Work
Concurrency is a first-class language feature, not only a library concern.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string, 1)
go func() {
time.Sleep(50 * time.Millisecond)
ch <- "done"
}()
fmt.Println(<-ch)
}gostarts a function concurrently; the runtime schedules it onto OS threads.- Channels pass data between goroutines and express ownership transfer.
- Always know how a goroutine exits - leaks show up as memory and FD growth.
Related: Why Go Exists - CSP-inspired concurrency model
5. defer for Reliable Cleanup
Defer registers cleanup that runs when the surrounding function returns.
package main
import (
"fmt"
"os"
)
func writeTemp() error {
f, err := os.CreateTemp("", "demo-*.txt")
if err != nil {
return err
}
defer func() {
f.Close()
os.Remove(f.Name())
}()
_, err = f.WriteString("hello")
return err
}
func main() {
if err := writeTemp(); err != nil {
panic(err)
}
fmt.Println("ok")
}deferpairs acquisition with release even when multiple return paths exist.- Defers run LIFO at function exit - useful for nested locks and files.
- Prefer defer over manual cleanup duplication in long functions.
6. Standard Library Over Framework Sprawl
Go ships batteries for HTTP, JSON, TLS, and testing in the toolchain you already have.
package main
import (
"encoding/json"
"net/http"
)
type Health struct {
Status string `json:"status"`
}
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(Health{Status: "ok"})
})
http.ListenAndServe(":8080", nil)
}net/httpis production-capable for many services before you reach for Gin, Echo, or chi.- Fewer dependencies mean smaller audit surfaces and faster
go testin CI. - Frameworks still help when you need opinionated routing, binding, or middleware ecosystems.
Related: Go's Target Domains - cloud-native defaults
7. Table-Driven Tests Encode Team Expectations
Tests document behavior as data rows, matching Go's preference for explicitness.
package mathx
import "testing"
func Add(a, b int) int { return a + b }
func TestAdd(t *testing.T) {
cases := []struct {
a, b, want int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tc := range cases {
if got := Add(tc.a, tc.b); got != tc.want {
t.Fatalf("Add(%d,%d)=%d, want %d", tc.a, tc.b, got, tc.want)
}
}
}- Table tests scale to edge cases without copy-paste assertion blocks.
go testis the universal CI entry point - no extra test runner required.- Failing cases name themselves through data, which aids code review.
Intermediate Examples
8. context.Context at API Boundaries
Cancellation and deadlines propagate through services the Go way.
package fetch
import (
"context"
"io"
"net/http"
"time"
)
func Get(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func GetWithTimeout(url string, d time.Duration) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), d)
defer cancel()
return Get(ctx, url)
}- Context is the standard first parameter for I/O-bound APIs.
- Parents cancel children when clients disconnect or deadlines elapse.
- Passing
context.Background()only at the outermost main or test harness keeps trees coherent.
Related: Go's Target Domains - distributed service boundaries
9. Accept Interfaces, Return Structs
Public functions depend on narrow interfaces; constructors expose concrete types.
package storage
import "context"
type Store interface {
Put(ctx context.Context, key string, value []byte) error
}
type FileStore struct {
dir string
}
func NewFileStore(dir string) *FileStore {
return &FileStore{dir: dir}
}
func (f *FileStore) Put(ctx context.Context, key string, value []byte) error {
// implementation omitted
return nil
}
func Save(ctx context.Context, s Store, key string, value []byte) error {
return s.Put(ctx, key, value)
}- Callers of
SavemockStorewithout importingFileStoreinternals. - Constructors (
NewFileStore) document valid initialization in one place. - This is the idiomatic alternative to factory-heavy DI frameworks.
Related: Simplicity as a Deliberate Constraint in API Design - shaping stable package APIs
10. Decision Sketch - When Not to Default to Go
Philosophy includes knowing where another language wins.
// Go excels: static binary CLI with flags and subcommands.
// Consider Rust: firmware-adjacent performance with no GC.
// Consider Python: exploratory data science with notebook iteration.
// Consider JVM: mature enterprise integrations your team already operates.
package main
import "fmt"
func main() {
fmt.Println("choose Go when team velocity and operability dominate the ADR")
}- Match language to dominant risk: latency floor, talent pool, ecosystem packages, or compliance tooling.
- Go is a strong default for networked services - not a universal hammer.
- Document the decision in an ADR so future teams inherit the reasoning.
Related: When Go Is the Wrong Tool - concrete counter-examples | Go vs Rust - systems trade-offs
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).