Package Design — Architecture and Structural Principles
Good package design is the cornerstone of maintainable and extensible Go code. This section covers patterns and principles accumulated by the Go community.
internal Package
Packages inside an internal directory can only be imported within the same module.
myproject/
├── go.mod
├── main.go
├── api/
│ └── handler.go # import "myproject/internal/auth" ✅
├── internal/
│ ├── auth/
│ │ └── auth.go # Cannot be imported externally
│ ├── db/
│ │ └── db.go
│ └── config/
│ └── config.go
└── pkg/
└── validator/
└── validator.go # Can be imported externally
// internal/auth/auth.go
package auth
import (
"errors"
"time"
)
// Token — public within the module but protected from external access
type Token struct {
UserID string
ExpiresAt time.Time
}
var ErrExpiredToken = errors.New("token has expired")
func Validate(token string) (*Token, error) {
if token == "" {
return nil, ErrExpiredToken
}
return &Token{UserID: "user-123", ExpiresAt: time.Now().Add(1 * time.Hour)}, nil
}
// Attempting to import from an external module causes a compile error:
// import "myproject/internal/auth"
// → use of internal package myproject/internal/auth not allowed
Preventing Circular Dependencies
Go does not allow circular dependencies.
❌ Circular dependency:
package user → imports package order
package order → imports package user ← Compile error!
✅ Solution 1: Extract interface
package user → imports package store (interface)
package order → imports package store (interface)
package userstore → implements store interface
package orderstore → implements store interface
✅ Solution 2: Common types package
package types (contains only shared types)
package user → imports types
package order → imports types
// ✅ Resolve circular dependency with interfaces
// pkg/store/store.go — defines only interfaces
package store
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type User struct {
ID string
Name string
}
type OrderStore interface {
GetOrder(id string) (*Order, error)
SaveOrder(order *Order) error
}
type Order struct {
ID string
UserID string
Amount float64
}
// internal/service/user.go — depends on store interface
package service
import "myproject/pkg/store"
type UserService struct {
userStore store.UserStore
orderStore store.OrderStore
}
func NewUserService(us store.UserStore, os store.OrderStore) *UserService {
return &UserService{userStore: us, orderStore: os}
}
func (s *UserService) GetUserWithOrders(userID string) (*store.User, []*store.Order, error) {
user, err := s.userStore.GetUser(userID)
if err != nil {
return nil, nil, err
}
return user, nil, nil
}
Layered Architecture
A layered architecture commonly used in real-world projects.
┌─────────────────────────────────────┐
│ Handler (API Layer) │ ← HTTP request/response
├─────────────────────────────────────┤
│ Service (Business Logic) │ ← Business rules
├─────────────────────────────────────┤
│ Repository (Data Access) │ ← DB/external API access
├─────────────────────────────────────┤
│ Domain (Models) │ ← Core domain types
└─────────────────────────────────────┘
// domain/user.go
package domain
import "time"
type User struct {
ID string
Email string
Name string
CreatedAt time.Time
}
type UserRepository interface {
FindByID(id string) (*User, error)
FindByEmail(email string) (*User, error)
Save(user *User) error
Delete(id string) error
}
// repository/user_repo.go
package repository
import (
"database/sql"
"myproject/domain"
)
type userRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) domain.UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByID(id string) (*domain.User, error) {
var u domain.User
err := r.db.QueryRow(
"SELECT id, email, name, created_at FROM users WHERE id = $1", id,
).Scan(&u.ID, &u.Email, &u.Name, &u.CreatedAt)
if err != nil {
return nil, err
}
return &u, nil
}
func (r *userRepository) FindByEmail(email string) (*domain.User, error) {
return nil, nil
}
func (r *userRepository) Save(user *domain.User) error {
return nil
}
func (r *userRepository) Delete(id string) error {
return nil
}
// service/user_service.go
package service
import (
"errors"
"myproject/domain"
)
type UserService struct {
repo domain.UserRepository
}
func NewUserService(repo domain.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id string) (*domain.User, error) {
if id == "" {
return nil, errors.New("user ID is required")
}
return s.repo.FindByID(id)
}
func (s *UserService) RegisterUser(email, name string) (*domain.User, error) {
existing, _ := s.repo.FindByEmail(email)
if existing != nil {
return nil, errors.New("email already registered")
}
user := &domain.User{
ID: generateID(),
Email: email,
Name: name,
}
return user, s.repo.Save(user)
}
func generateID() string {
return "user-" + "generated-id" // Use UUID in real code
}
// api/handler.go
package api
import (
"encoding/json"
"net/http"
"myproject/service"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(us *service.UserService) *UserHandler {
return &UserHandler{userService: us}
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id") // Go 1.22+
user, err := h.userService.GetUser(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
Package Design Principles
1. Single Responsibility Principle
// ❌ Too many responsibilities
package utils
func ParseJSON(data []byte) (interface{}, error) { ... }
func SendEmail(to, subject, body string) error { ... }
func HashPassword(pw string) string { ... }
func ValidatePhone(phone string) bool { ... }
// ✅ Split by responsibility
package json // JSON parsing
package email // Email sending
package crypto // Encryption
package validator // Validation
2. Name Packages by Role
// ❌ Verb-form package names (Go convention violation)
package convertstring
package handleusers
// ✅ Noun-form, short and clear
package strconv // string conversion
package user // user domain
3. Small Interfaces
// ❌ Interface too large
type UserManager interface {
Create(user User) error
Read(id string) (User, error)
Update(user User) error
Delete(id string) error
List() ([]User, error)
Authenticate(email, password string) (Token, error)
ResetPassword(email string) error
// ... continues
}
// ✅ Just what's needed
type UserReader interface {
FindByID(id string) (*User, error)
}
type UserWriter interface {
Save(user *User) error
Delete(id string) error
}
// Compose when needed
type UserRepository interface {
UserReader
UserWriter
}
Directory Structure — Standard Layout
A project structure widely used in the Go community.
myproject/
├── cmd/ # Executable binaries
│ └── server/
│ └── main.go
├── internal/ # Internal packages
│ ├── config/
│ ├── middleware/
│ └── repository/
├── pkg/ # Publicly exported packages
│ └── validator/
├── api/ # API definitions (OpenAPI, Proto, etc.)
├── web/ # Frontend assets
├── scripts/ # Build/deploy scripts
├── docs/ # Documentation
├── go.mod
├── go.sum
└── Makefile
Key Takeaways
- internal package: Protects API boundary by blocking external imports
- Preventing circular deps: Extract interfaces or use a shared types package
- Layered architecture: Handler → Service → Repository → Domain
- Small interfaces: Include only necessary methods, extend through composition
- Package names are nouns: Short and clear, avoid plurals and common prefixes