Go: Go Project Architecture
Last updated: 2026-08-26
Go doesn't have a project structure enforced by a إطار عمل—but that means you have to design it yourself. A well-thought-out architecture keeps your project clear when it's 1,000 lines long and still manageable when it reaches 100,000 lines.
When you take over a project where all the code is crammed into main.go, the first step in refactoring is to move the code into the correct directories.
1. You will learn
- Standard Go project directory structure:
cmd/,internal/,pkg/ - The Clean Architecture 4-Layer Model
- Dependency Injection (Constructor Injection)
- Incorrect type definition
- Setting Up the Project Framework
2. A True Story of a Backend Engineer
(1) Pain Point: 100,000 lines of code are all in main.go
Alice took over an e-commerce project:
"My predecessor put all the code in
main.go: route registration, قاعدة بيانات operations, HTML templates, and business logic—all jumbled together in 30,000 lines. When I wanted to add a user حالة field, I had to make changes in five different places to get it right. It took me two weeks to add a feature, and three days just to find the code."
// Bad code: everything piled into main.go
package main
var db *sql.DB
func main() {
// Database connection
// Route registration
// HTML templates
// User handler
// Order handler
// Product handler
// All mixed together!
}
// To find "user" related code: Ctrl+F search for "user", scattered across 50 places
(2) Go's Solution: 4-Layer Architecture
myapp/
├── cmd/
│ └── server/
│ └── main.go # Entry → dependency injection + start service
├── internal/
│ ├── handler/ # Layer 1: HTTP handlers (parse request / return response)
│ ├── service/ # Layer 2: Business logic (domain rules)
│ └── repository/ # Layer 3: Data access (database / external API)
├── pkg/
│ └── model/ # Layer 4: Domain models (data structures)
└── go.mod
(3) Returns: Chaos vs. Stratification
| Scenario | Chaotic Architecture | 4-Layer Architecture |
|---|---|---|
| New fields | 5 changes at unknown locations | Changes only to model + handler |
| Switch Databases | Update All Database Calls | Update Only the Repository Layer |
| Unit Testing | Cannot Perform Unit Testing | Each Layer Can Be Tested Independently Using Mocks |
| Getting Started for Newcomers | Browsing Through 30,000 Lines of main.go | Understanding Responsibilities Just by Looking at Directory Names |
3. Standard Project Layout
(1) A Detailed Explanation of the Directory Structure
myproject/
├── cmd/ # Executable entry points
│ ├── server/ # server binary
│ │ └── main.go
│ └── migrate/ # database migration tool
│ └── main.go
├── internal/ # Private packages (cannot be imported externally)
│ ├── handler/ # HTTP handlers
│ ├── service/ # Business logic
│ └── repository/ # Data access
├── pkg/ # Exportable public packages
│ └── model/ # Domain models
├── migrations/ # SQL migration files
├── config/ # Configuration files
├── scripts/ # Helper scripts
├── go.mod
└── go.sum
(2) cmd/ / internal/ / pkg/ Responsibilities
| Directory | Uses | Can Be Imported Externally |
|---|---|---|
cmd/ |
Executable entry point (main function) | N/A (it is the main package) |
internal/ |
Private implementation | ❌ External imports are prohibited by the Go compiler |
pkg/ |
Public code exposed to the outside world | ✅ |
internal directory is a special directory for the Go compiler—no package outside the parent directory of internal can import it. This provides true encapsulation, which is more secure than "naming conventions" (such as _private).
4. The Clean Architecture 4-Layer Model
handler (HTTP) → service (business) → repository (data)
↓ ↓ ↓
Request parsing Domain rules Database/API
Response return Transaction mgmt CRUD operations
Param validation Multi-step orchestration Cache access
▶ Example: 4-layer implementation
// ---------- Layer 4: Model (domain models) ----------
// pkg/model/user.go
package model
type User struct {
ID int
Name سلسلة
Email سلسلة
CreatedAt time.Time
}
type CreateUserRequest struct {
Name سلسلة
Email سلسلة
}
// ---------- Layer 3: Repository (data access) ----------
// internal/repository/user.go
package repository
import (
"قاعدة بيانات/sql"
"myapp/pkg/model"
)
type UserRepository interface {
FindByID(id int) (*model.User, خطأ)
Create(req model.CreateUserRequest) (*model.User, خطأ)
List() ([]*model.User, خطأ)
Delete(id int) خطأ
}
type userRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByID(id int) (*model.User, خطأ) {
row := r.db.QueryRow("SELECT id, name, email, created_at FROM users WHERE id = ?", id)
user := &model.User{}
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return user, err
}
func (r *userRepository) Create(req model.CreateUserRequest) (*model.User, خطأ) {
result, err := r.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", req.Name, req.Email)
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
return r.FindByID(int(id))
}
// ---------- Layer 2: Service (business logic) ----------
// internal/service/user.go
package service
import (
"errors"
"myapp/internal/repository"
"myapp/pkg/model"
"strings"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidName = errors.New("name is required")
ErrInvalidEmail = errors.New("invalid email format")
)
type UserService struct {
repo repository.UserRepository
}
func NewUserService(repo repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) Create(req model.CreateUserRequest) (*model.User, خطأ) {
if strings.TrimSpace(req.Name) == "" {
return nil, ErrInvalidName
}
if !strings.Contains(req.Email, "@") {
return nil, ErrInvalidEmail
}
return s.repo.Create(req)
}
func (s *UserService) Get(id int) (*model.User, خطأ) {
user, err := s.repo.FindByID(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
// ---------- Layer 1: Handler (HTTP handler) ----------
// internal/handler/user.go
package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"myapp/internal/service"
"myapp/pkg/model"
)
type UserHandler struct {
svc *service.UserService
}
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req model.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
user, err := h.svc.Create(req)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, user)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
user, err := h.svc.Get(id)
if errors.Is(err, service.ErrUserNotFound) {
writeError(w, http.StatusNotFound, err.Error())
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "internal خطأ")
return
}
writeJSON(w, http.StatusOK, user)
}
// ---------- Utility functions ----------
func writeJSON(w http.ResponseWriter, حالة int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(حالة)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, حالة int, msg سلسلة) {
writeJSON(w, حالة, map[سلسلة]سلسلة{"خطأ": msg})
}
(2) Direction of Dependency
Handler → Service → Repository (→ DB)
| | |
↓ ↓ ↓
Model Model Model
| Layer | Responsibilities | Dependencies | Testable |
|---|---|---|---|
| Handler | HTTP Parsing/Response | Service | Mock Service |
| Service | Business Rules/Orchestration | Repository | Mock Repository |
| Repository | Data CRUD | قاعدة بيانات/sql | Mock DB / Integration Testing |
| Model | Data Structure | None | — |
5. Dependency Injection
▶ Example: Constructor Injection
⚙️ Prerequisite: Run
go get github.com/mattn/go-sqlite3(requires CGO; alternatively usemodernc.org/sqlitefor a pure Go driver)
// cmd/server/main.go
package main
import (
"database/sql"
"log"
"net/http"
"myapp/internal/handler"
"myapp/internal/repository"
"myapp/internal/service"
)
func main() {
// ---- Dependency injection (assemble all layers) ----
// 1. Database connection
db, err := sql.Open("sqlite3", "./app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 2. Repository layer
userRepo := repository.NewUserRepository(db)
orderRepo := repository.NewOrderRepository(db)
// 3. Service layer (depends on Repository)
userSvc := service.NewUserService(userRepo)
orderSvc := service.NewOrderService(orderRepo, userRepo)
// 4. Handler layer (depends on Service)
userHandler := handler.NewUserHandler(userSvc)
orderHandler := handler.NewOrderHandler(orderSvc)
// 5. Route registration
mux := http.NewServeMux()
userHandler.Register(mux, "/api/v1/users")
orderHandler.Register(mux, "/api/v1/orders")
log.Println("Service listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
(2) Comparison of Dependency Injection Approaches
| Method | Implementation | Advantages | Disadvantages |
|---|---|---|---|
| Constructor injection | NewXxx(dep) |
Explicit, compile-time checking | Long code due to dependencies |
| Manual implementation | Assembly of the main function | No third-party libraries required | Cumbersome to maintain in large projects |
| Google Wire | Code Generation | Automatic Injection | Learning Curve |
github.com/google/wire) to automatically generate dependency injection code.
6. Complete Example: E-commerce Project Framework
▶ Example: Full Implementation
// cmd/خادم/main.go
package main
import (
"context"
"قاعدة بيانات/sql"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
_ "github.com/mattn/go-sqlite3"
)
// ---------- Model (pkg/model) ----------
type Product struct {
ID int `json:"id"`
Name سلسلة `json:"name"`
Price float64 `json:"price"`
}
type Order struct {
ID int `json:"id"`
UserID int `json:"user_id"`
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
Total float64 `json:"total"`
Status سلسلة `json:"حالة"`
CreatedAt time.Time `json:"created_at"`
}
// ---------- Repository (internal/repository) ----------
type ProductRepository struct {
db *sql.DB
}
func NewProductRepository(db *sql.DB) *ProductRepository {
return &ProductRepository{db: db}
}
func (r *ProductRepository) FindByID(id int) (*Product, خطأ) {
p := &Product{}
err := r.db.QueryRow("SELECT id, name, price FROM products WHERE id = ?", id).
Scan(&p.ID, &p.Name, &p.Price)
if err == sql.ErrNoRows {
return nil, nil
}
return p, err
}
func (r *ProductRepository) List() ([]*Product, خطأ) {
rows, err := r.db.Query("SELECT id, name, price FROM products")
if err != nil {
return nil, err
}
defer rows.Close()
var products []*Product
for rows.Next() {
p := &Product{}
if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
return nil, err
}
products = append(products, p)
}
return products, rows.Err()
}
type OrderRepository struct {
db *sql.DB
}
func NewOrderRepository(db *sql.DB) *OrderRepository {
return &OrderRepository{db: db}
}
func (r *OrderRepository) Create(order *Order) خطأ {
result, err := r.db.Exec(
"INSERT INTO orders (user_id, product_id, quantity, total, حالة) VALUES (?, ?, ?, ?, ?)",
order.UserID, order.ProductID, order.Quantity, order.Total, order.Status,
)
if err != nil {
return err
}
id, _ := result.LastInsertId()
order.ID = int(id)
return nil
}
func (r *OrderRepository) FindByID(id int) (*Order, خطأ) {
o := &Order{}
err := r.db.QueryRow(
"SELECT id, user_id, product_id, quantity, total, حالة, created_at FROM orders WHERE id = ?", id,
).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.Total, &o.Status, &o.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return o, err
}
// ---------- Service (internal/service) ----------
type OrderService struct {
productRepo *ProductRepository
orderRepo *OrderRepository
}
func NewOrderService(productRepo *ProductRepository, orderRepo *OrderRepository) *OrderService {
return &OrderService{
productRepo: productRepo,
orderRepo: orderRepo,
}
}
type PlaceOrderInput struct {
UserID int
ProductID int
Quantity int
}
var (
ErrProductNotFound = &AppError{Code: "PRODUCT_NOT_FOUND", Message: "product not found", HTTPStatus: 404}
ErrInsufficientStock = &AppError{Code: "INSUFFICIENT_STOCK", Message: "insufficient stock", HTTPStatus: 409}
)
type AppError struct {
Code سلسلة `json:"code"`
Message سلسلة `json:"message"`
HTTPStatus int `json:"-"`
}
func (e *AppError) Error() سلسلة {
return e.Message
}
func (s *OrderService) PlaceOrder(input PlaceOrderInput) (*Order, خطأ) {
product, err := s.productRepo.FindByID(input.ProductID)
if err != nil {
return nil, err
}
if product == nil {
return nil, ErrProductNotFound
}
total := product.Price * float64(input.Quantity)
order := &Order{
UserID: input.UserID,
ProductID: input.ProductID,
Quantity: input.Quantity,
Total: total,
Status: "created",
}
if err := s.orderRepo.Create(order); err != nil {
return nil, err
}
return order, nil
}
// ---------- Handler (internal/handler) ----------
type OrderHandler struct {
svc *OrderService
}
func NewOrderHandler(svc *OrderService) *OrderHandler {
return &OrderHandler{svc: svc}
}
func (h *OrderHandler) Register(mux *http.ServeMux, basePath سلسلة) {
mux.HandleFunc("POST "+basePath, h.PlaceOrder)
mux.HandleFunc("GET "+basePath+"/{id}", h.GetOrder)
}
type placeOrderRequest struct {
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
}
func (h *OrderHandler) PlaceOrder(w http.ResponseWriter, r *http.Request) {
// Get user ID from Context (injected by auth وسيط)
userID, ok := r.Context().Value("user_id").(int)
if !ok {
writeError(w, http.StatusUnauthorized, "not authenticated")
return
}
var req placeOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
order, err := h.svc.PlaceOrder(PlaceOrderInput{
UserID: userID,
ProductID: req.ProductID,
Quantity: req.Quantity,
})
if appErr, ok := err.(*AppError); ok {
writeError(w, appErr.HTTPStatus, appErr.Message)
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "internal خطأ")
return
}
writeJSON(w, http.StatusCreated, order)
}
func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
// ... business logic
}
func writeJSON(w http.ResponseWriter, حالة int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(حالة)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, حالة int, msg سلسلة) {
writeJSON(w, حالة, map[سلسلة]سلسلة{"خطأ": msg})
}
// ---------- Main (dependency injection entry) ----------
func main() {
db, err := sql.Open("sqlite3", "./shop.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Initialize tables
db.Exec(`CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, price REAL
)`)
db.Exec(`CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER,
product_id INTEGER, quantity INTEGER, total REAL,
حالة TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
// Dependency injection
productRepo := NewProductRepository(db)
orderRepo := NewOrderRepository(db)
orderSvc := NewOrderService(productRepo, orderRepo)
orderHandler := NewOrderHandler(orderSvc)
mux := http.NewServeMux()
orderHandler.Register(mux, "/api/v1/orders")
// Health check
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[سلسلة]سلسلة{"حالة": "ok"})
})
خادم := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
خادم.Shutdown(ctx)
}()
log.Println("E-commerce service listening on :8080")
if err := خادم.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
graph TB
subgraph Handler
H[HTTP Handler]
end
subgraph Service
S[Business Logic]
end
subgraph Repository
R[Data Access]
end
subgraph Model
M[Domain Structs]
end
subgraph External ["External Dependencies"]
DB[(Database)]
end
H -->|calls| S
S -->|calls| R
R -->|queries| DB
H --- M
S --- M
R --- M
style Handler fill:#e1f5fe
style Service fill:#fff3e0
style Repository fill:#e8f5e9
style Model fill:#f3e5f5
❓ FAQ
NewXxx(dep1, dep2) *Xxx. All dependencies are created and assembled in the main دالة (or in the code generated by Wire). Advantages: (1) Clear dependency relationships; (2) Compile-time checks; (3) Ability to pass mocks during unit testing.internal directory?internal directory can only be imported by code in their parent directory. This provides true encapsulation—external users cannot import internal packages. In large projects, this prevents architectural violations (such as handlers being directly imported into repository).📖 Summary
- Standard تخطيط:
cmd/(entry point),internal/(private),pkg/(public) - 4-layer model: Handler → Service → Repository → Model
- Dependency direction: from outer to inner; the inner layer is unaware of the outer layer
- Dependency Injection: Constructor Injection (Most Common)
- Interface Decoupling: Go's Implicit Interfaces Make Dependency Inversion Natural
- Error type: Custom AppError containing Code/Message/HTTPStatus
internaldirectory: Encapsulation enforced by the Go compiler
📝 Exercises
-
Basic (Difficulty ⭐): Create a project skeleton containing the following directories: cmd/خادم/main.go, internal/handler/, internal/service/, internal/repository/, and pkg/model/. Implement a simple
HealthHandlerthat returns{"حالة": "ok"}. -
Advanced (Difficulty ⭐⭐): Implement Product Category Management using a 4-tier architecture. Requirements: (1) Model: Category(id, name, parent_id); (2) Handler: CRUD endpoints; (3) Service: Validate that category names are unique and prevent circular references; (4) Repository: Implemented using SQLite; (5) The
mainدالة assembles the components via constructor injection. -
Challenge (Difficulty ⭐⭐⭐): Refactor the library management system from Lesson 22 into a four-tier architecture. Requirements: (1) Move the code to the
cmd/internal/pkgdirectory; (2) The Handler layer should handle only HTTP; (3) The Service layer must include complete business validation; (4) The Repository layer must use SQLite; (5) Expose the Repository as an interface to enable mock testing; (6) Write unit tests (using a mock repository) to test the business rules in the Service layer.