PAPER
What Your Go Code Looks Like When the System Is Unclear
Most discussions about Go focus on performance, simplicity, or concurrency.
That misses the more useful signal.
Go code makes it easy to see whether the underlying system is understood. Not because the language enforces correctness, but because it leaves very little room to hide uncertainty.
The shape of the code reflects the clarity of the system behind it.
You can see whether a system is understood by looking at the shape of its code.
Go makes this especially visible.
The God Function Is Not the Problem
func ProcessTransaction(ctx context.Context, req Request) error {
if req.Amount <= 0 {
return errors.New("invalid amount")
}
user, err := s.userRepo.Get(req.UserID)
if err != nil {
return err
}
if user.IsBlocked {
return errors.New("user blocked")
}
if err := s.limitService.Check(user, req.Amount); err != nil {
return err
}
if req.Type == "international" {
if err := s.fxService.ApplyRate(&req); err != nil {
return err
}
}
if err := s.ledger.Debit(user.AccountID, req.Amount); err != nil {
return err
}
if err := s.audit.Log(req); err != nil {
return err
}
return nil
}
This function is not large enough to be obviously wrong. It compiles. It works. It passes tests.
The problem is not size. It is responsibility.
Validation, policy enforcement, conditional branching, side effects, and auditing are all embedded in a single flow. The system's boundaries are not visible. They are implied.
This is what unclear systems look like in Go. Not broken. Just dense.
Extraction Does Not Fix It
The common response is to extract functions.
func ProcessTransaction(ctx context.Context, req Request) error {
if err := validate(req); err != nil {
return err
}
user, err := fetchUser(req.UserID)
if err != nil {
return err
}
if err := enforcePolicy(user, req); err != nil {
return err
}
if err := applyEffects(user, req); err != nil {
return err
}
return nil
}
This looks cleaner. It is not clearer.
The same decisions still exist. They are now distributed across functions without changing ownership or boundaries. The system is still being discovered during execution.
Abstraction without a boundary is just movement.
Error Handling as Noise
if err != nil {
return err
}
Repeated enough times, this stops being meaningful.
Not because errors are unimportant, but because they are not being interpreted. Every failure is treated the same. Errors are propagated, not interpreted. There is no distinction between invalid input, dependency failure, or system inconsistency.
The code acknowledges failure. It does not explain it. The system stops, but it does not decide.
When everything is handled the same way, nothing is understood.
Where the Shape Changes
Now compare:
type Decision struct {
Approved bool
Reason string
}
func (d DecisionEngine) Evaluate(input Input) Decision {
if input.Amount <= 0 {
return Decision{false, "invalid amount"}
}
if input.UserBlocked {
return Decision{false, "user blocked"}
}
if input.ExceedsLimit {
return Decision{false, "limit exceeded"}
}
return Decision{true, "approved"}
}
func (s Service) Execute(ctx context.Context, input Input) error {
decision := s.engine.Evaluate(input)
if !decision.Approved {
return ErrRejected(decision.Reason)
}
if err := s.ledger.Debit(input.AccountID, input.Amount); err != nil {
return err
}
return s.audit.Log(input)
}
The difference is not style. It is separation.
Decisions are deterministic. Effects are isolated. Boundaries are visible.
The system is no longer embedded in a flow. It is expressed.
What the Code Is Telling You
You do not need a design document to understand a system. Look at the code.
Look at whether decisions are mixed with side effects. Look at whether errors are classified or just propagated. Look at whether reuse blurs ownership. Look at whether functions represent behavior or steps.
These are not style issues. They are signals.
What the Language Exposes
Go does not enforce structure. It exposes whether structure exists.
If the system is unclear, the code will be dense, repetitive, and difficult to reason about. If the system is understood, the code becomes simpler, not because the language made it so, but because there is less ambiguity to express.
The language did not change. The system did.
Continue reading
Papers