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


Packages are directories


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