What Go optimises for
Go was designed at Google for large teams, code that lives a long time, and fast builds. Most of its decisions follow from that.
| Principle | In practice |
|---|---|
| Simplicity | Few keywords and features. If there were two ways to do something, one was removed. |
| Readability first | Code is read far more often than it is written. Clear beats clever; “boring” is a compliment. |
| Composition | No class hierarchies. You combine small types and small interfaces. |
| Explicit over magic | No annotations and no hidden control flow. Errors are ordinary return values. |
| Concurrency built in | Goroutines and channels are part of the language, not a library. |
| Fast tooling | Builds take seconds, the output is one static binary, and there is one official formatter. |
| Useful zero values | nil, "", 0, false; well-designed types work without initialisation. |
| Batteries included | A rich standard library (HTTP, JSON, crypto, testing, profiling) and small third-party libraries instead of frameworks. |
Object orientation without classes
Go has types, methods and interfaces, but no classes and no inheritance. The usual concepts still have a place:
| Concept | How Go does it |
|---|---|
| Class | Named type (usually a struct) + methods |
| Encapsulation | Lowercase identifiers; the package is the boundary |
| Abstraction | Interface (small, declared by the consumer) |
| Reuse | Struct embedding (composition + method promotion) |
| Overriding | A method on the outer type shadows the embedded one |
| Calling the “parent” | Explicit call through the embedded field: r.BaseRepo.Save(o) |
| Dynamic dispatch | Interface values (the only kind there is) |
| Combining contracts | Interface embedding: interface { Reader; Writer } |
| Parametric polymorphism | Type parameters (Go 1.18+) |
| Overloading | Not supported: different names, variadics or options |
| Constructor | NewX() function, composite literal, or the zero value |
| Static members | Package-level variables and functions |
| Runtime type checks | Type assertion x.(T), type switch |
| Enums | Typed constants with iota |
| String form / equality | String() (fmt.Stringer), == on comparable structs |
| Callbacks | Closures and function types |
Details in Types, Interfaces & Composition.
Common design problems, Go idioms
Most classic design patterns shrink to a function or an interface.
| Problem | Go idiom |
|---|---|
| Shared algorithm, varying steps | A function that takes an interface |
| Swappable behaviour (strategy) | A function type or a one-method interface |
| Wrapping behaviour (decorator, middleware) | A type that holds an interface and implements it again; func(http.Handler) http.Handler |
| Many optional parameters | Functional options, or a Config/Deps struct |
| One shared instance | Package-level var + sync.Once / sync.OnceValue |
| Event listener | Closure |
| Constructing variants | NewX() returning an interface or struct |
| Dependency injection | Pass dependencies into NewX() explicitly in main |
Dependency injection is just wiring in main:
store := postgres.Open(ctx, dsn)
svc := service.New(store, clock, logger)
api := httpapi.New(httpapi.Deps{Service: svc, Log: logger})
No container, no reflection. The compiler checks the whole graph.
Think in Go
- Start concrete. Write structs and functions first. Introduce an interface when a second consumer actually needs one.
- Handle errors where they occur. Add context with
%w. Don’t reach forpanic. - Prefer boring code. If it needs a comment to be understood, make it simpler instead.
- Let
gofmtdecide formatting. No style debates. - Measure before you optimise. The profiler regularly disagrees with the obvious fix (see Testing & Profiling).
Where to go next
- A Tour of Go
- Effective Go
- Go by Example
- 100 Go Mistakes and How to Avoid Them (Teiva Harsanyi)