Skip to main content

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:

bash
# 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:

go
package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

To run this application:

bash
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:

go
// 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.

go
// 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:

go
// 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:

go
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:

  1. cmd/api/main.go - The entry point for the REST API server
  2. cmd/worker/main.go - The entry point for a background worker
  3. internal/handler - HTTP handlers that process requests
  4. internal/middleware - HTTP middleware (authentication, logging, etc.)
  5. internal/model - Data models and business logic
  6. internal/repository - Data access layer
  7. internal/service - Business logic connecting handlers and repositories
  8. pkg/logger - Reusable logging package
  9. pkg/validator - Reusable validation utilities

Example Code: REST API Service

Here's a simplified implementation of this structure:

go
// 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)
}
}
go
// 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:

bash
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

go
// 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)
}
go
// 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:

bash
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:

yaml
# 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:

go
// 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:

  1. The basic elements of a Go project (go.mod, main.go)
  2. The standard Go project layout with directories like cmd/, internal/, and pkg/
  3. Best practices for organizing your Go code
  4. 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

Practice Exercises

  1. Basic Structure: Create a simple "Hello, World!" Go application using the structure outlined in this guide.
  2. CLI Tool: Build a command-line tool with at least two commands using the cmd/ directory structure.
  3. Web Service: Implement a basic REST API following the standard project layout.
  4. Library: Create a reusable library in the pkg/ directory and use it in a main application.
  5. 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! :)