No overloading, no default arguments
One name, one signature. Use an options struct or functional options:
type Option func(*Server)
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second}
for _, o := range opts {
o(s)
}
return s
}
srv := NewServer(":8080", WithTimeout(5*time.Second))
A nil pointer in an interface is not a nil interface
func find() error {
var e *MyError // nil
return e // interface holding (*MyError, nil)
}
find() == nil // false (!)
An interface is (type, value); it’s only nil when both are. Return a literal nil on success.
Slices share backing arrays
a := []int{1, 2, 3, 4}
b := a[:2]
b = append(b, 99)
fmt.Println(a) // [1 2 99 4]: append wrote into a's array
append may or may not copy depending on capacity. Use slices.Clone when you need independence.
Maps aren’t thread-safe
Concurrent writes crash the process with fatal error: concurrent map writes, which can’t be recovered. Guard with sync.Mutex/RWMutex, or use sync.Map for append-mostly caches.
Map iteration order is deliberately randomised: never rely on it. Use slices.Sorted(maps.Keys(m)) when order matters.
Value semantics
Structs are copied on assignment and when passed by value:
s := Session{State: Lobby}
t := s
t.State = Open
// s.State is still Lobby
for _, v := range items gives copies. Modify in place via the index: items[i].X = ....
Missing constructs
- No ternary: use
if/else. - No
while:for cond { }andfor { }cover everything. - No exceptions: see Errors.
- No
null-safe operators: check fornilexplicitly, or design types whose zero value is valid.
Packages are directories
- Import cycles are a compile error. Design package boundaries early; push shared types down into a leaf package.
- Unused imports and unused local variables are compile errors, not warnings.
Generics are minimal
Since 1.18, type parameters exist for containers and algorithms. No generic methods on types, no class hierarchies. If you’re reaching for generics to model behaviour, an interface is probably the answer.
Small surprises
| Surprise | Why |
|---|---|
defer in a loop doesn’t run until the function returns |
Wrap the loop body in a function |
time.Time has a monotonic clock reading |
Compare with t.Equal(u), not == |
| String indexing gives bytes, not characters | for _, r := range s iterates runes |
x.(T) panics on mismatch |
Use v, ok := x.(T) |
Shadowing with := in an inner scope |
err declared inside if hides the outer err |
| JSON ignores unexported fields | Only capitalized fields are (un)marshalled |