Hello everyone,
I’m seeing a common pattern in many large Go projects (including some well-known ones) where single source files within a package often grow beyond the 1000-line mark.
On one hand, the general advice from veterans like Uncle Bob is to keep functions short, simple, and readable, and use the SRP (Single Responsibility Principle).
On the other hand, the Go focus on packages often encourages keeping all related logic (types, interfaces, methods) in close proximity, frequently in the same file.
Does this widespread practice of having ‘large source files’ contradict the core simplicity and readability philosophy of Go?
I’d love to hear your thoughts: What is your personal line in the sand regarding file size, and when is it most appropriate to split a file within the same package for improved maintainability? struct per file ?
Does this widespread practice of having ‘large source files’ contradict the core simplicity and readability philosophy of Go
I don’t think it does. As you said, putting something close to whatever it relates to, makes it easier for people who read the code to rational about what they’re seeing.
Idiomatic Go packages are actually an excellent example of single-responsibility, portability, and putting thought into the surface area of your exported types. Consider:
Don’t use a single package for all your APIs. Many well-intentioned programmers put all the interfaces exposed by their program into a single package named api, types, or interfaces, thinking it makes it easier to find the entry points to their code base. This is a mistake. Such packages suffer from the same problems as those named util or common, growing without bound, providing no guidance to users, accumulating dependencies, and colliding with other imports. Break them up, perhaps using directories to separate public packages from implementation.
Thinking about your package names and API surface area leads to tightly-scoped, well defined packages / responsibilities. And putting everything in the same file? I don’t do that. I don’t think that is idiomatic. Take a look at the files in encoding/json:
I don’t see a single file in that folder that I can’t reason about its’ purpose based on the name. And the package is clearly broken out into many files.
I don’t think this is a widespread practice in go any more than it is in other languages (and often less so in go in my experience). Take the json example above from go stdlib and compare it to this rust json crate:
Specifically compare deserialize to decode. You tell me which is easier to reason about. But surely front-end devs have things figured out and know how to keep things simple, right?? Wrong:
Start bouncing around that repo. Things are exported and re-exported so much you will quickly find yourself chasing your own tail. And even still they have monolithic files.
In summary: having to switch between files too often can be obnoxious and make things feel a bit fragmented. Having monolithic files can feel overwhelming. There is, for sure, a balance there. I generally try to group things as logically as possible and refactor judiciously along the way. I don’t think any single language/community/ecosystem gets things 100% right (because everything is a series of tradeoffs) but I do, generally, find idiomatic go packages easy to reason about compared to many other languages/ecosystems I use.