pkg & internal directories are way overused
TLDR; Opting for a flatter structure typically guides you organically away from pkg and overly nested internal.
pkg is now used considerably less than it used to be. But it's still fairly common. Mostly because lots of folks coming to Go encounter the standard package layout repo and copy the structure.
pkg is an old vestige from the GOPATH era. Then other large projects like k8s went w/ the structure & never changed it because of the size of the projects and their backward compat promises.
But that doesn't mean we should copy that w/o asking why. The extra layer of nesting makes imports ugly and code discovery harder. pkg pretty much has no place in today's greenfield projects. But I also don't go around rooting it out of existing projects.
Another common one is abusing the /internal directory in svcs. I review a ton of candidate assignment submissions at work, and it's pretty common to see the entire app code shoved into the internal directory. The common rationale is abstraction.
But if it's application code, then no one is importing it. So shoving everything into the internal package is lazy design instead of properly structuring the core subpackages.
In libraries, internal makes sense to keep implementation details away from users of the API. Even there, creating three layers of nesting is a code smell.
Code organization is subjective & contexual. Giving general advice is extremely hard. But one thing I have found teachable is this:
- Go already doesn't allow import cycles
- On top of that, you should design your packages so that dependencies flow inwards. This means outer/adaptor packages can depend on core packages, but core packages should not depend on the outer ones.
myapp/
order/
order.go // package order: Order, LineItem, Status
repository.go // package order: Repository interface
postgres/
order_repository.go // package postgres, imports "myapp/order"
http/
order_handler.go // package http, imports "myapp/order"
Here, postgres and http can import order, but order doesn't import either of them. The core package doesn't know / care whether it is being used by HTTP, Postgres, Kafka, or anything else.
This is the only generalized rule that goes a long way for most applications and libraries. Otherwise, eschewing unnecessary nesting and gauging the smell from import paths is the way to go.
Another thing I've found quite helpful is running go doc continuously while developing something. This way, I can assess whether the public API looks ugly and overwhelming from a user's perspective.