One of the most frustrating things I encountered when I first started with Go was the lack of a “standard” framework-mandated directory structure. Coming from Rails or Django, I expected a folder to be told to me. In Go, you’re given a blank slate, which is liberating until you realize you’ve created a massive main.go file that’s 3,000 lines long and impossible to test.
Over the last few years, through dozens of microservices and a few monolithic mistakes, I’ve converged on a set of golang project structure best practices that prioritize maintainability and prevent the dreaded circular dependency. If you’re staring at a new project and wondering where to put your logic, here are my top tips.
1. Use the /cmd Directory for Entry Points
I always place my main applications in a /cmd folder. Each subdirectory under /cmd represents a separate binary. For example, if your project has a web server and a CLI migration tool, your structure should look like this:
/cmd
/server
main.go
/migrate
main.go
This keeps your root directory clean and prevents the root package from becoming a dumping ground for configuration and setup logic.
2. Protect Your Logic with /internal
The /internal directory is a special folder in Go. The Go compiler prevents other projects from importing packages inside /internal. I use this for 90% of my application logic. By placing your business logic here, you explicitly signal that this code is not a public API and can be refactored without breaking downstream users.
3. Separate Domain Logic from Infrastructure
One of the biggest mistakes I see is mixing database queries with business rules. To avoid this, I follow a simplified version of golang clean architecture. I group my code by responsibility rather than function. Instead of a /models folder, I use feature-based packages like /internal/user or /internal/payment.
4. Keep the Root Package Lean
Your root directory should only contain project-wide configuration files like go.mod, Makefile, README.md, and .gitignore. If you find yourself adding utils.go to the root, it’s time to create a package. A lean root makes it much easier for new developers to understand the project’s entry points at a glance.
5. Define Interfaces Where They Are Used
In many languages, you define an interface alongside the implementation. In Go, I’ve found that defining interfaces in the package that consumes the dependency leads to much better decoupling. This is a core part of my golang testing best practices, as it makes mocking dependencies for unit tests trivial.
6. Use /pkg for Shared Utilities
While /internal is for private code, /pkg is for code that is safe for other projects to import. Be careful here—I’ve found that overusing /pkg often leads to “utility hell.” Only move code to /pkg if you are certain it is a generic tool that provides value outside the context of your specific application.
7. Avoid the ‘Common’ or ‘Utils’ Package
I used to have a /internal/utils package. It eventually became a junk drawer for everything from string manipulation to date formatting. Instead, name your packages by what they do. Instead of utils.ParseDate(), use date.Parse(). This makes your imports readable and your dependencies explicit.
8. Group by Feature, Not by Layer
Avoid the “Java style” of /services, /repositories, and /controllers folders. This forces you to jump between three different directories just to change one field in a user profile. Instead, group by feature:
/internal
/user
repository.go
service.go
handler.go
/billing
repository.go
service.go
9. Implement a Strong Configuration Pattern
Don’t scatter os.Getenv calls throughout your codebase. I prefer creating a config package that loads all environment variables into a typed struct at startup. This provides a single source of truth and allows you to fail fast if a required variable is missing.
10. Standardize Your Error Handling
Consistency in project structure extends to how you handle errors. I recommend creating custom error types within your feature packages. This allows your handlers to differentiate between a “Not Found” error (404) and a “Validation” error (400) without relying on fragile string matching.
As shown in the image below, keeping these boundaries clear is the difference between a project that scales and one that becomes a maintenance nightmare.
Ready to put these structures into practice? Check out my detailed golang clean architecture tutorial to see how to layer these folders for maximum flexibility.
Common Mistakes to Avoid
- Circular Dependencies: This usually happens when Package A imports Package B, and Package B imports Package A. The solution is usually to move the shared logic into a third, lower-level package.
- Deep Nesting: Don’t create
/internal/app/core/domain/user/service. Keep your nesting shallow. Three to four levels is usually the sweet spot. - Over-Engineering Early: Don’t implement a full hexaganal architecture for a simple CRUD app. Start lean, and refactor into these patterns as the complexity grows.
Measuring Success: How Do You Know It’s Working?
You’ll know your project structure is successful when:
- You can find the logic for a specific feature in under 10 seconds.
- Adding a new feature doesn’t require changing five different packages.
- Your unit tests can run without initializing the entire database (thanks to proper interface boundaries).
- Your
go mod graphdoesn’t look like a bowl of spaghetti.
If you’re struggling with flaky tests in your new structure, I highly recommend reading my guide on golang testing best practices to ensure your architecture supports testability.