Every job ad says it: Go developer, senior, hybrid. And every freelance ad says the opposite: contract-to-hire, remote, bring your own coffee. Somewhere in between stands the freelance Gopher, keyboard in one hand, invoice in the other, wondering why go mod tidy can solve a dependency graph in four seconds flat but nobody can solve a contract rate table in four weeks.

This is a field guide. Namespace: Go terms. You will be tested at compile time.

The go.mod Years

A freelance career begins the moment you stop being an employee and become a module. Here’s the thing about go.mod: it declares, up top, the minimum language version you require. Not the maximum. The minimum. Recruiters haven’t read this. They ask for “7+ years of Go,” a number reachable only by people who were writing Go at Cambridge while the language was still on a whiteboard.

Your own file, similarly, declares what you need:

module freelance/gopher

go 1.21

require (
    github.com/clients/unicorn v1.2.3
    github.com/clients/nightmare v2.0.1 // indirect
)

Note nightmare lives in // indirect. Nobody ever directly required nightmare work. It arrives as a dependency of dependencies, in your go.sum at 2 a.m., and it must be pinned or your whole build breaks. Freelancing is 20% coding and 80% telling PMs why the build broke.

And the golden rule of versions applies to contracts too: you cannot downgrade a signed SOW. MVS just means hourly rates only ever go up.

defer Now or Never

Go’s defer runs at function exit, not at scope exit. Junior developers learn this the hard way. Freelancers learn it the hard way too, except the function is your career and the exit is the handshake.

Every payment term must be a defer:

func doWork(client Client) error {
    deliverables, err := ship(client.Spec)
    if err != nil {
        return err
    }
    defer client.Invoice(deliverables) // runs at exit, but log it first
    return client.Integrate()
}

Did the integration succeed? Invoiced. Did the client ghost you mid-project? Invoiced. defer does not negotiate, which is more than can be said for accounts payable, whose runtime keeps panicking in a goroutine nobody waits on. Errors from deferred calls are silently swallowed unless you check. So check.

Goroutines and scope creep

We mentioned panicking in a goroutine nobody joins. Let’s dwell there, because “nobody joins it” is the single most common bug in independent contracting. A goroutine has nothing to join. Nothing to join is what freelancing is. But your work still needs to terminate.

Scope creep arrives as a friend: “while you’re in there.” And every freelancer knows the code shape:

for {
    select {
    case <-ctx.Done():
        return ctx.Err()
    case task := <-extra(askedFor):
        // one more "tiny" favor, forever
    }
}

Your original contract was the context. The moment “tiny favors” arrive on the default case, you’ve leaked a goroutine: doing unbounded unpaid work with no cancellation in sight. The fix is the same as in production; make the extra requests a new channel, i.e. a change request, with its own deadline and its own budget. Then defer cancel() the old arrangement and wait for the client to acknowledge.

What We Learned

Be a good module: declare your minimums, keep your go.sum honest, and let MVS keep your rate on the floor where it belongs.

Invoice like defer: automatic, unconditional, checked for errors.

And terminate your goroutines: every “quick favor” without a context deadline is a leak, and leaked goroutines never merge back to main.

Now if you’ll excuse me, my go vet --verbosity=client-relationship just fired. Apparently “I’ll pay you soon” fails the format check.