Go Project Layout
Introduction
When starting a new Go project, one of the first decisions you'll face is how to organize your code. A well-structured project makes your code more maintainable, readable, and collaborative. This guide will walk you through common Go project layouts, conventions, and best practices that will help you build scalable applications.
Unlike some programming languages, Go doesn't enforce a strict project structure. However, the Go community has developed conventions that have become standard practices. Understanding these patterns will help you create projects that are familiar to other Go developers and leverage the language's built-in tools effectively.
Basic Project Structure
Let's start with a minimal Go project structure:
my-go-project/
├── go.mod # Module definition
├── go.sum # Dependency checksums
├── main.go # Application entry point
├── README.md # Project documentation
└── LICENSE # License information
This simple structure works well for small applications or utilities. The main.go
file contains your application's entry point with the main()
function, while go.mod
defines your module and manages dependencies.
Example: Creating a Basic Go Project
Let's create a simple "Hello, World!" application with this structure:
# Create a directory for your project
mkdir hello-world
cd hello-world
# Initialize a Go module
go mod init github.com/yourusername/hello-world
# Create main.go file
touch main.go
Now, let's write a simple program in main.go
:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
To run this application:
go run main.go
Output:
Hello, World!
Standard Go Project Layout
As your project grows, you'll need a more organized structure. The Go community has developed a standard project layout that works well for most applications:
my-go-project/
├── cmd/ # Main applications
│ └── myapp/ # Application-specific directory
│ └── main.go # Application entry point
├── internal/ # Private code
│ ├── app/ # Application code
│ └── pkg/ # Private library code
├── pkg/ # Public library code
├── api/ # API specifications (OpenAPI, Protobuf, etc.)
├── configs/ # Configuration files
├── docs/ # Documentation
├── examples/ # Example code
├── scripts/ # Build and CI scripts
├── test/ # Test data and additional test utilities
├── web/ # Web assets (templates, static files)
├── go.mod # Module definition
├── go.sum # Dependency checksums
└── README.md # Project documentation
Let's explore each of these directories in detail:
cmd/
The cmd
directory contains the main applications for the project. Each subdirectory represents a separate executable application:
// cmd/myapp/main.go
package main
import (
"fmt"
"github.com/yourusername/my-go-project/internal/app"
)
func main() {
app := app.NewApp()
fmt.Println(app.Greeting("world"))
}
This structure is particularly useful when your project has multiple entry points or binaries.
internal/
The internal
directory contains code that's specific to your application and shouldn't be imported by other projects. Go's compiler enforces this restriction - any package path containing /internal/
can only be imported by code in the same module.
// internal/app/app.go
package app
type App struct{}
func NewApp() *App {
return &App{}
}
func (a *App) Greeting(name string) string {
return "Hello, " + name + "!"
}
pkg/
The pkg
directory contains code that's safe for external projects to import:
// pkg/greeting/greeting.go
package greeting
// GenerateGreeting creates a greeting message for the given name
func GenerateGreeting(name string) string {
return "Hello, " + name + "!"
}
Other projects can now import and use this code:
import "github.com/yourusername/my-go-project/pkg/greeting"
Project Layout Example
Let's see a more complete example of a REST API service using this layout:
flowchart TD A[my-service] --> B[cmd] A --> C[internal] A --> D[pkg] A --> E[api] A --> F[configs]
B --> G[api/main.go] B --> H[worker/main.go]
C --> I[handler] C --> J[middleware] C --> K[model] C --> L[repository] C --> M[service]
D --> N[logger] D --> O[validator]
Let's walk through what each component does:
cmd/api/main.go
- The entry point for the REST API servercmd/worker/main.go
- The entry point for a background workerinternal/handler
- HTTP handlers that process requestsinternal/middleware
- HTTP middleware (authentication, logging, etc.)internal/model
- Data models and business logicinternal/repository
- Data access layerinternal/service
- Business logic connecting handlers and repositoriespkg/logger
- Reusable logging packagepkg/validator
- Reusable validation utilities
Example Code: REST API Service
Here's a simplified implementation of this structure:
// cmd/api/main.go
package main
import (
"log"
"net/http"
"github.com/yourusername/my-service/internal/handler"
"github.com/yourusername/my-service/internal/repository"
"github.com/yourusername/my-service/internal/service"
"github.com/yourusername/my-service/pkg/logger"
)
func main() {
// Initialize components
log := logger.New()
repo := repository.NewUserRepository()
svc := service.NewUserService(repo)
handler := handler.NewUserHandler(svc, log)
// Set up HTTP routes
http.HandleFunc("/users", handler.GetUsers)
// Start server
log.Info("Starting server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("Server failed to start", err)
}
}
// internal/handler/user.go
package handler
import (
"encoding/json"
"net/http"
"github.com/yourusername/my-service/internal/service"
"github.com/yourusername/my-service/pkg/logger"
)
type UserHandler struct {
service service.UserService
logger *logger.Logger
}
func NewUserHandler(svc service.UserService, log *logger.Logger) *UserHandler {
return &UserHandler{
service: svc,
logger: log,
}
}
func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
users, err := h.service.GetAllUsers()
if err != nil {
h.logger.Error("Failed to get users", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
Best Practices for Go Project Layout
1. Use Go Modules
Always use Go modules for dependency management. Initialize your project with:
go mod init github.com/yourusername/your-project
2. Follow the Package Design Principles
- Package by layer: Group code by its role (handlers, services, repositories)
- Package by feature: Group code by feature or domain (users, products, orders)
3. Avoid Circular Dependencies
Circular dependencies can lead to compilation errors and indicate design issues. Keep your package hierarchy clean and unidirectional.
4. Use /internal
for Private Code
Place code that shouldn't be imported by other projects in the /internal
directory.
5. Use /pkg
Sparingly
Only put code in /pkg
if it's meant to be reused by external projects.
6. Keep the Main Function Simple
The main.go
file should focus on initialization and wiring components together, with most logic delegated to other packages.
Real-World Example: Building a CLI Tool
Let's create a simple CLI tool that fetches weather data, following our project structure:
weather-cli/
├── cmd/
│ └── weather/
│ └── main.go
├── internal/
│ ├── client/
│ │ └── weather.go
│ └── formatter/
│ └── output.go
├── pkg/
│ └── location/
│ └── geocode.go
├── go.mod
└── go.sum
Implementation Example
// cmd/weather/main.go
package main
import (
"flag"
"fmt"
"os"
"github.com/yourusername/weather-cli/internal/client"
"github.com/yourusername/weather-cli/internal/formatter"
"github.com/yourusername/weather-cli/pkg/location"
)
func main() {
city := flag.String("city", "", "City to get weather for")
flag.Parse()
if *city == "" {
fmt.Println("Please provide a city using the -city flag")
os.Exit(1)
}
// Get coordinates for the city
coords, err := location.GetCoordinates(*city)
if err != nil {
fmt.Printf("Error getting location: %v
", err)
os.Exit(1)
}
// Get weather data
weatherClient := client.NewWeatherClient()
weather, err := weatherClient.GetWeather(coords)
if err != nil {
fmt.Printf("Error getting weather: %v
", err)
os.Exit(1)
}
// Format and display weather
output := formatter.FormatWeather(weather)
fmt.Println(output)
}
// internal/client/weather.go
package client
import (
"encoding/json"
"fmt"
"net/http"
"github.com/yourusername/weather-cli/pkg/location"
)
type WeatherClient struct {
apiKey string
baseURL string
}
type WeatherData struct {
Temperature float64
Conditions string
Humidity int
}
func NewWeatherClient() *WeatherClient {
return &WeatherClient{
apiKey: "your-api-key", // In production, use environment variables
baseURL: "https://api.example.com/weather",
}
}
func (c *WeatherClient) GetWeather(loc location.Coordinates) (*WeatherData, error) {
url := fmt.Sprintf("%s?lat=%f&lon=%f&appid=%s",
c.baseURL, loc.Latitude, loc.Longitude, c.apiKey)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// In a real app, parse the actual JSON response
// This is simplified for the example
weather := &WeatherData{
Temperature: 22.5,
Conditions: "Sunny",
Humidity: 65,
}
return weather, nil
}
When you run the program with:
go run cmd/weather/main.go -city "New York"
Output:
Weather for New York:
Temperature: 22.5°C
Conditions: Sunny
Humidity: 65%
Handling Configuration
For configuration, you can use the configs
directory:
my-go-project/
├── configs/
│ ├── development.yaml
│ └── production.yaml
Example configuration file:
# configs/development.yaml
server:
port: 8080
timeout: 30s
database:
host: localhost
port: 5432
user: postgres
password: postgres
name: myapp_dev
logging:
level: debug
You can load this configuration in your application:
// internal/config/config.go
package config
import (
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
Server struct {
Port int `yaml:"port"`
Timeout string `yaml:"timeout"`
} `yaml:"server"`
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
Name string `yaml:"name"`
} `yaml:"database"`
Logging struct {
Level string `yaml:"level"`
} `yaml:"logging"`
}
func Load(path string) (*Config, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var cfg Config
decoder := yaml.NewDecoder(f)
if err := decoder.Decode(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
Summary
A well-structured Go project makes your code more maintainable, readable, and collaborative. In this guide, we've explored:
- The basic elements of a Go project (go.mod, main.go)
- The standard Go project layout with directories like cmd/, internal/, and pkg/
- Best practices for organizing your Go code
- Real-world examples showing how to implement these patterns
Remember, while these conventions are widely accepted in the Go community, you should adapt them to fit your specific needs. Start with a simple structure and evolve it as your project grows.
Additional Resources
- Standard Go Project Layout - A reference for standard Go project structure
- Go Modules Documentation - Official documentation for Go modules
- Effective Go - Guidelines for writing clear and idiomatic Go code
- Clean Architecture in Go - Applying clean architecture principles to Go projects
Practice Exercises
- Basic Structure: Create a simple "Hello, World!" Go application using the structure outlined in this guide.
- CLI Tool: Build a command-line tool with at least two commands using the cmd/ directory structure.
- Web Service: Implement a basic REST API following the standard project layout.
- Library: Create a reusable library in the pkg/ directory and use it in a main application.
- Configuration: Add configuration management to your application using the configs/ directory.
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)