Why I Stopped Using "Flat" Project Structures in Go
When I first started with Go, I loved how simple it was. I put my main.go, my database logic, and my HTML templates all in the root folder. It worked! Until I had more than five files. Then, it became a scavenger hunt.
If you are a junior developer, you’ve probably felt that "folder fatigue." Here is how I transitioned to a Standard Go Project Layout and why it changed my productivity.
a. The "Cmd" vs. "Internal" Divide
In Go, the separation of concerns isn't just a suggestion; the compiler actually helps you enforce it.
cmd/: This is for your entry points. If you have a web server and a CLI tool, they each get a subfolder here. No logic goes here—only "wiring" things together.
And then we have:
internal/: This is the secret sauce. Code inside internal/ cannot be imported by other projects. It’s the perfect place for your private business logic.
b. The "Dependency Injection" Habit
I used to initialize my database inside every function that needed it. Don't do this. It makes testing impossible because you can't run your code without a live database.
Instead, "inject" your dependencies through a constructor:
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
Now, when I write a unit test, I can pass in a mock database instead of the real thing. My tests went from taking 5 seconds to 5 milliseconds.
c. Avoiding the utils Package
We’ve all created a utils or common package where we dump everything from string formatting to math helpers. In Go, this usually leads to circular dependencies (where Package A imports Package B, which imports Package A), and your code won't compile.
You can fix this by naming your packages based on what they provide, not what they are. Instead of utils.ValidateEmail(), use auth.ValidateEmail().
Top comments (0)