Why This Matters
In a complex web application with authentication, database operations, API handlers, business logic, and utilities, improper organization leads to thousands of lines of tangled code. This makes it impossible to find anything, test components independently, or enable effective collaboration among multiple developers.
This is exactly the chaos that Go packages solve. They provide scalable organization that keeps large codebases manageable, allows teams to work independently, and enables proper separation of concerns.
Real-World Impact:
- Team Collaboration: Different teams own different packages
- Code Reusability: Share packages across multiple projects and organizations
- Testing: Test individual packages in isolation with clear boundaries
- Maintenance: Find and fix bugs quickly when code is well-organized
Packages are the foundation of building maintainable Go applications. Master them, and you'll build systems that scale gracefully instead of becoming unmaintainable.
Learning Objectives
By the end of this article, you'll master:
- Package Philosophy: Understand Go's approach to code organization and modularity
- Visibility Control: Master the capitalization system for public/private APIs
- Import Management: Organize dependencies efficiently and avoid common pitfalls
- Package Design: Create packages that are focused, reusable, and maintainable
- Project Structure: Organize large applications with proven architectural patterns
- Best Practices: Apply industry standards for naming, documentation, and testing
Core Concepts - Understanding Go's Package Philosophy
The Go Philosophy: Simplicity Meets Power
Go's package system was designed from day one to be simple yet powerful. Unlike languages that added modules as an afterthought, Go built packages into its foundation.
Key Design Principles:
- One package per directory - Clear, predictable organization
- Explicit imports - No hidden dependencies, everything is visible
- Capitalization-based visibility - Elegant, no keywords needed
- No circular imports - Forces good architectural decisions
Why This Matters:
- Predictable structure: Anyone can understand your project organization
- Forces good design: Circular dependencies are prevented at compile time
- Fast compilation: Clear dependency graph enables fast builds
- Team scalability: Clear boundaries allow parallel development
What Is a Package, Really?
A package is a collection of Go source files in the same directory that:
- Share the same package name
- Are compiled together as one unit
- Can access each other's unexported members
- Present a unified API to external code
Visualizing Package Structure:
myproject/
├── go.mod # Module definition
├── main.go # Entry point
├── utils/ # utils package directory
│ ├── strings.go # Part of utils package
│ ├── validation.go # Part of utils package
│ └── conversion.go # Part of utils package
└── auth/ # auth package directory
├── auth.go # Main auth logic
├── middleware.go # HTTP middleware
└── tokens.go # Token handling
Key Insights:
- All
.gofiles inutils/belong toutilspackage - Files within same package can access each other's private members
- External packages only see exported members
- Package boundary is enforced at compile time
Practical Examples - From Basic to Advanced
Example 1: Basic Package Creation
Let's start with the fundamentals - creating and using a simple utility package:
1// run
2package main
3
4import (
5 "fmt"
6)
7
8// Simulating mathutils package inline
9func Add(a, b int) int {
10 return a + b
11}
12
13func Multiply(a, b int) int {
14 return a * b
15}
16
17func Divide(a, b int) int {
18 if b == 0 {
19 return 0
20 }
21 return a / b
22}
23
24func main() {
25 // Use functions
26 sum := Add(10, 20)
27 fmt.Printf("10 + 20 = %d\n", sum)
28
29 product := Multiply(5, 6)
30 fmt.Printf("5 × 6 = %d\n", product)
31
32 result := Divide(10, 0)
33 fmt.Printf("10 ÷ 0 = %d (safe handling)\n", result)
34
35 result2 := Divide(10, 2)
36 fmt.Printf("10 ÷ 2 = %d\n", result2)
37}
What's happening:
- Functions like
Add,Multiply,Dividewould be exported from amathutilspackage - Exported functions are accessible from other packages
- Internal validation ensures safe operations
Example 2: Package with Structs and Methods
Let's build a user package that demonstrates encapsulation and clean API design:
1// run
2package main
3
4import (
5 "fmt"
6 "regexp"
7 "time"
8)
9
10// User represents a user in our system
11// Only exported fields are accessible from outside the package
12type User struct {
13 id int // Private
14 Email string // Public
15 Name string // Public
16 password string // Private
17 createdAt time.Time // Private
18}
19
20// NewUser creates a new user with validation
21func NewUser(email, name, password string) (*User, error) {
22 if err := validateUserData(email, name, password); err != nil {
23 return nil, err
24 }
25
26 u := &User{
27 id: generateID(),
28 Email: email,
29 Name: name,
30 password: hashPassword(password),
31 createdAt: time.Now(),
32 }
33
34 return u, nil
35}
36
37// GetEmail returns the user's email
38func (u *User) GetEmail() string {
39 return u.Email
40}
41
42// UpdateEmail changes the user's email with validation
43func (u *User) UpdateEmail(newEmail string) error {
44 if !isValidEmail(newEmail) {
45 return fmt.Errorf("invalid email format")
46 }
47 u.Email = newEmail
48 return nil
49}
50
51// CheckPassword verifies the password
52func (u *User) CheckPassword(password string) bool {
53 return hashPassword(password) == u.password
54}
55
56// validateUserData validates user input (private)
57func validateUserData(email, name, password string) error {
58 if email == "" || name == "" || password == "" {
59 return fmt.Errorf("all fields are required")
60 }
61
62 if !isValidEmail(email) {
63 return fmt.Errorf("invalid email format")
64 }
65
66 if len(password) < 8 {
67 return fmt.Errorf("password must be at least 8 characters")
68 }
69
70 return nil
71}
72
73// isValidEmail checks email format (private)
74func isValidEmail(email string) bool {
75 emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
76 matched, _ := regexp.MatchString(emailRegex, email)
77 return matched
78}
79
80// hashPassword simulates password hashing (private)
81func hashPassword(password string) string {
82 return "hashed_" + password
83}
84
85// generateID simulates ID generation (private)
86func generateID() int {
87 return 12345
88}
89
90func main() {
91 // Create user using exported constructor
92 newUser, err := NewUser("alice@example.com", "Alice Smith", "secret123")
93 if err != nil {
94 fmt.Printf("Error creating user: %v\n", err)
95 return
96 }
97
98 fmt.Printf("Created user: Email=%s, Name=%s\n", newUser.Email, newUser.Name)
99
100 // Use exported methods
101 if err := newUser.UpdateEmail("alice.new@example.com"); err != nil {
102 fmt.Printf("Error updating email: %v\n", err)
103 } else {
104 fmt.Printf("Updated email: %s\n", newUser.GetEmail())
105 }
106
107 // Verify password
108 if newUser.CheckPassword("secret123") {
109 fmt.Println("Password verification: SUCCESS")
110 } else {
111 fmt.Println("Password verification: FAILED")
112 }
113}
Key Concepts Demonstrated:
- Encapsulation: Private fields hidden from external code
- Controlled access: Methods manage field access
- Input validation: Private helpers ensure data integrity
- Clean constructor:
NewUser()creates properly initialized instances
Example 3: Multi-Package Project Structure
Let's build a realistic project structure showing how packages work together:
1// run
2package main
3
4import (
5 "fmt"
6 "time"
7)
8
9// Config represents application configuration
10type Config struct {
11 Port string
12 DatabaseURL string
13 JWTSecret string
14 Debug bool
15}
16
17// LoadConfig simulates loading configuration
18func LoadConfig() (*Config, error) {
19 return &Config{
20 Port: "8080",
21 DatabaseURL: "postgres://localhost:5432/mydb",
22 JWTSecret: "secret-key-123",
23 Debug: true,
24 }, nil
25}
26
27// Database connection simulation
28type Database struct {
29 url string
30}
31
32func NewDatabase(url string) (*Database, error) {
33 return &Database{url: url}, nil
34}
35
36func (db *Database) Close() error {
37 fmt.Println("Database connection closed")
38 return nil
39}
40
41func (db *Database) Ping() error {
42 fmt.Printf("Database ping successful: %s\n", db.url)
43 return nil
44}
45
46// UserRepository simulates user data access
47type UserRepository struct {
48 db *Database
49}
50
51func NewUserRepository(db *Database) *UserRepository {
52 return &UserRepository{db: db}
53}
54
55// UserService simulates user business logic
56type UserService struct {
57 repo *UserRepository
58}
59
60func NewUserService(repo *UserRepository) *UserService {
61 return &UserService{repo: repo}
62}
63
64// Router simulates HTTP routing
65type Router struct {
66 userService *UserService
67 jwtSecret string
68}
69
70func NewRouter(userService *UserService, jwtSecret string) *Router {
71 return &Router{
72 userService: userService,
73 jwtSecret: jwtSecret,
74 }
75}
76
77func (r *Router) RouteCount() int {
78 return 10 // Simulated route count
79}
80
81func main() {
82 // Load configuration
83 cfg, err := LoadConfig()
84 if err != nil {
85 fmt.Printf("Failed to load config: %v\n", err)
86 return
87 }
88
89 // Initialize database
90 db, err := NewDatabase(cfg.DatabaseURL)
91 if err != nil {
92 fmt.Printf("Failed to connect to database: %v\n", err)
93 return
94 }
95 defer db.Close()
96
97 // Test database connection
98 if err := db.Ping(); err != nil {
99 fmt.Printf("Database ping failed: %v\n", err)
100 return
101 }
102
103 // Create repository and service layers
104 userRepo := NewUserRepository(db)
105 userService := NewUserService(userRepo)
106
107 // Create HTTP router
108 router := NewRouter(userService, cfg.JWTSecret)
109
110 fmt.Printf("Server starting on port %s\n", cfg.Port)
111 fmt.Printf("Database connected: %s\n", cfg.DatabaseURL)
112 fmt.Printf("Handlers configured for %d routes\n", router.RouteCount())
113 fmt.Printf("Debug mode: %t\n", cfg.Debug)
114}
What this demonstrates:
- Separation of concerns: Each component has clear responsibility
- Dependency injection: Components are composed together
- Clean interfaces: Packages expose minimal, focused APIs
- Testable design: Each layer can be tested independently
Common Patterns and Pitfalls
Pattern 1: Domain-Driven Package Organization
Organize packages by business domain rather than technical layers:
Good: Domain-driven organization
myproject/
├── user/ # All user-related functionality
│ ├── user.go # User model and business logic
│ ├── repository.go # Data access for users
│ └── service.go # User service layer
├── order/ # All order-related functionality
│ ├── order.go
│ ├── repository.go
│ └── service.go
├── payment/ # All payment-related functionality
│ ├── payment.go
│ ├── processor.go
│ └── gateway.go
└── shipping/ # All shipping-related functionality
├── shipping.go
└── calculator.go
Avoid: Technical layer organization
myproject/
├── models/ # Data models
├── repositories/ # Data access
├── services/ # Business logic
└── controllers/ # HTTP handlers
Benefits of Domain-Driven:
- Clear ownership: Each team owns specific domain packages
- Low coupling: Domains interact through well-defined interfaces
- Easy testing: Each domain can be tested in isolation
- Scalable teams: Multiple teams can work on different domains
Pattern 2: The internal Directory for Private Code
Use Go's special internal/ directory for implementation details:
myproject/
├── cmd/ # Application entry points
│ ├── server/main.go
│ └── worker/main.go
├── internal/ # Private code
│ ├── auth/ # Authentication logic
│ │ ├── auth.go
│ │ └── middleware.go
│ ├── database/ # Database operations
│ │ ├── connection.go
│ │ └── migrations.go
│ └── business/ # Business logic
│ ├── users.go
│ └── orders.go
├── pkg/ # Public reusable code
│ ├── logger/ # Logging utilities
│ │ └── logger.go
│ └── validator/ # Validation utilities
│ └── validator.go
├── api/ # API specifications
│ └── openapi.yaml
└── go.mod
Why this works:
internal/packages can only be imported by code in parent directories- External projects cannot import
myproject/internal/auth - Prevents exposing implementation details
- Forces clean API boundaries
Pattern 3: Package Naming Conventions
Follow Go's established naming conventions:
1// Good: Short, descriptive, lowercase names
2package http // Standard library
3package json // Standard library
4package strings // Standard library
5package validator // Third-party package
6package logger // Your package
7
8// Avoid: Poor naming practices
9// package HTTP // Not lowercase
10// package my_package // Uses underscores
11// package utility // Too generic
12// package data // Too vague
13// package helpers // Not descriptive
Naming Guidelines:
- Lowercase single words when possible
- Descriptive but concise -
validatornotvalidations - Avoid abbreviations -
httpclientnothttpcli - Use domain names -
auth,user,payment - Follow Go standard library style
Pattern 4: Import Organization
Organize imports clearly and consistently:
1// run
2package main
3
4import (
5 // Standard library imports
6 "context"
7 "fmt"
8 "net/http"
9 "time"
10)
11
12func main() {
13 fmt.Println("=== Import Organization Example ===")
14
15 // Create a context with timeout
16 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
17 defer cancel()
18
19 // Simulate HTTP request
20 req, err := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil)
21 if err != nil {
22 fmt.Printf("Error creating request: %v\n", err)
23 return
24 }
25
26 fmt.Printf("Request created: %s %s\n", req.Method, req.URL)
27 fmt.Println("Imports are organized: stdlib first, then third-party, then local")
28}
Import Organization Best Practices:
- Standard library imports first
- Third-party imports second
- Local imports last
- Blank line between groups
- Alphabetically sorted within each group
Pitfall 1: Circular Imports
Go prevents circular imports at compile time. Here's how to avoid them:
Problem: Circular dependency
1// Package auth imports user
2// package auth
3// import "myproject/user"
4
5// Package user imports auth
6// package user
7// import "myproject/auth"
Solution: Use shared interfaces or move common logic
1// run
2package main
3
4import "fmt"
5
6// Common package with shared interfaces
7type UserValidator interface {
8 ValidateUser(userID int) bool
9 ValidateEmail(email string) bool
10}
11
12// Auth package would implement validation
13type AuthService struct{}
14
15func (a *AuthService) ValidateUser(userID int) bool {
16 return userID > 0
17}
18
19func (a *AuthService) ValidateEmail(email string) bool {
20 return len(email) > 0
21}
22
23// User service depends on interface, not concrete type
24type UserService struct {
25 validator UserValidator
26}
27
28func NewUserService(validator UserValidator) *UserService {
29 return &UserService{validator: validator}
30}
31
32func (u *UserService) CreateUser(email string) error {
33 if !u.validator.ValidateEmail(email) {
34 return fmt.Errorf("invalid email")
35 }
36 fmt.Printf("User created with email: %s\n", email)
37 return nil
38}
39
40func main() {
41 auth := &AuthService{}
42 userService := NewUserService(auth)
43
44 if err := userService.CreateUser("test@example.com"); err != nil {
45 fmt.Printf("Error: %v\n", err)
46 }
47}
Pitfall 2: Interface Pollution
Don't create interfaces until you actually need them:
1// run
2package main
3
4import "fmt"
5
6// Avoid: Unnecessary interface when only one implementation exists
7// type UserRepository interface {
8// Save(user *User) error
9// Find(id int) (*User, error)
10// }
11
12// Better: Use concrete types directly
13type User struct {
14 ID int
15 Name string
16}
17
18type UserRepository struct {
19 users map[int]*User
20}
21
22func NewUserRepository() *UserRepository {
23 return &UserRepository{
24 users: make(map[int]*User),
25 }
26}
27
28func (r *UserRepository) Save(user *User) error {
29 r.users[user.ID] = user
30 return nil
31}
32
33func (r *UserRepository) Find(id int) (*User, error) {
34 user, exists := r.users[id]
35 if !exists {
36 return nil, fmt.Errorf("user not found")
37 }
38 return user, nil
39}
40
41func main() {
42 repo := NewUserRepository()
43
44 user := &User{ID: 1, Name: "Alice"}
45 repo.Save(user)
46
47 found, err := repo.Find(1)
48 if err != nil {
49 fmt.Printf("Error: %v\n", err)
50 return
51 }
52
53 fmt.Printf("Found user: %+v\n", found)
54}
Rule of thumb: Wait until you have 2+ implementations before creating an interface.
Pitfall 3: God Packages
Avoid packages that do too much:
1// run
2package main
3
4import "fmt"
5
6// Avoid: God package with too many responsibilities
7// package utils would contain:
8// - User management
9// - Payment processing
10// - Email sending
11// - Reporting
12// - Validation
13
14// Better: Focused packages
15type UserManager struct{}
16
17func (u *UserManager) CreateUser(name string) {
18 fmt.Printf("User created: %s\n", name)
19}
20
21type PaymentProcessor struct{}
22
23func (p *PaymentProcessor) ProcessPayment(amount float64) {
24 fmt.Printf("Payment processed: $%.2f\n", amount)
25}
26
27type EmailSender struct{}
28
29func (e *EmailSender) SendEmail(to, subject string) {
30 fmt.Printf("Email sent to %s: %s\n", to, subject)
31}
32
33func main() {
34 userMgr := &UserManager{}
35 userMgr.CreateUser("Alice")
36
37 payment := &PaymentProcessor{}
38 payment.ProcessPayment(99.99)
39
40 email := &EmailSender{}
41 email.SendEmail("alice@example.com", "Welcome!")
42}
Integration and Mastery - Building Real Applications
Master Example: Complete Web Application Architecture
Let's build a complete mini e-commerce application showing advanced package organization:
1// run
2package main
3
4import (
5 "context"
6 "fmt"
7 "os"
8 "os/signal"
9 "sync"
10 "syscall"
11 "time"
12)
13
14// Config represents application configuration
15type Config struct {
16 Port string
17 DatabaseURL string
18 JWTSecret string
19 JWTExpiration time.Duration
20 SMTPServer string
21 Environment string
22 ServiceName string
23 ServiceVersion string
24 LogLevel string
25 LogFormat string
26 HealthPort string
27 MaxDBConnections int
28 RecommendEngine string
29}
30
31// LoadFromEnv loads configuration from environment
32func LoadFromEnv() (*Config, error) {
33 return &Config{
34 Port: "8080",
35 DatabaseURL: "postgres://localhost:5432/ecommerce",
36 JWTSecret: "secret-jwt-key",
37 JWTExpiration: 24 * time.Hour,
38 SMTPServer: "smtp.example.com",
39 Environment: "development",
40 ServiceName: "ecommerce-api",
41 ServiceVersion: "1.0.0",
42 LogLevel: "info",
43 LogFormat: "json",
44 HealthPort: "8081",
45 MaxDBConnections: 10,
46 RecommendEngine: "collaborative",
47 }, nil
48}
49
50// Logger simulates structured logging
51type Logger struct {
52 level string
53 format string
54}
55
56func NewLogger(level, format string) (*Logger, error) {
57 return &Logger{level: level, format: format}, nil
58}
59
60func (l *Logger) Info(msg string, args ...interface{}) {
61 fmt.Printf("[INFO] %s\n", fmt.Sprintf(msg, args...))
62}
63
64func (l *Logger) Error(msg string, args ...interface{}) {
65 fmt.Printf("[ERROR] %s\n", fmt.Sprintf(msg, args...))
66}
67
68// Database simulates database connection
69type Database struct {
70 url string
71}
72
73func NewDatabase(url string, maxConns int) (*Database, error) {
74 return &Database{url: url}, nil
75}
76
77func (db *Database) Migrate(ctx context.Context) error {
78 fmt.Println("Running database migrations...")
79 time.Sleep(100 * time.Millisecond)
80 return nil
81}
82
83func (db *Database) Close() error {
84 fmt.Println("Database connection closed")
85 return nil
86}
87
88// AuthService simulates authentication
89type AuthService struct {
90 db *Database
91 jwtSecret string
92 expiration time.Duration
93}
94
95func NewAuthService(db *Database, secret string, exp time.Duration) *AuthService {
96 return &AuthService{
97 db: db,
98 jwtSecret: secret,
99 expiration: exp,
100 }
101}
102
103// EmailService simulates email sending
104type EmailService struct {
105 smtpServer string
106}
107
108func NewEmailService(smtp string) *EmailService {
109 return &EmailService{smtpServer: smtp}
110}
111
112// RecommendationService simulates product recommendations
113type RecommendationService struct {
114 db *Database
115 engine string
116}
117
118func NewRecommendationService(db *Database, engine string) *RecommendationService {
119 return &RecommendationService{db: db, engine: engine}
120}
121
122func (r *RecommendationService) Start(ctx context.Context) error {
123 fmt.Printf("Starting recommendation engine: %s\n", r.engine)
124 return nil
125}
126
127func (r *RecommendationService) Shutdown(ctx context.Context) error {
128 fmt.Println("Shutting down recommendation engine")
129 return nil
130}
131
132// APIServer simulates HTTP server
133type APIServer struct {
134 config *Config
135 logger *Logger
136 auth *AuthService
137 email *EmailService
138 recomm *RecommendationService
139}
140
141func NewAPIServer(cfg *Config, logger *Logger, auth *AuthService, email *EmailService, recomm *RecommendationService) *APIServer {
142 return &APIServer{
143 config: cfg,
144 logger: logger,
145 auth: auth,
146 email: email,
147 recomm: recomm,
148 }
149}
150
151func (s *APIServer) Start(ctx context.Context) error {
152 s.logger.Info("API server started on port %s", s.config.Port)
153 <-ctx.Done()
154 return nil
155}
156
157func (s *APIServer) Shutdown(ctx context.Context) error {
158 s.logger.Info("Shutting down API server")
159 return nil
160}
161
162// HealthChecker simulates health check endpoint
163type HealthChecker struct {
164 db *Database
165 environment string
166 serviceName string
167 version string
168}
169
170func NewHealthChecker(db *Database, env, name, version string) *HealthChecker {
171 return &HealthChecker{
172 db: db,
173 environment: env,
174 serviceName: name,
175 version: version,
176 }
177}
178
179func (h *HealthChecker) Start(ctx context.Context, port string) error {
180 fmt.Printf("Health check server started on port %s\n", port)
181 <-ctx.Done()
182 return nil
183}
184
185// Application orchestrates all components
186type Application struct {
187 config *Config
188 server *APIServer
189 database *Database
190 auth *AuthService
191 email *EmailService
192 recomm *RecommendationService
193 health *HealthChecker
194 logger *Logger
195}
196
197func NewApplication() (*Application, error) {
198 // Load configuration
199 cfg, err := LoadFromEnv()
200 if err != nil {
201 return nil, fmt.Errorf("failed to load config: %w", err)
202 }
203
204 // Initialize logger
205 logger, err := NewLogger(cfg.LogLevel, cfg.LogFormat)
206 if err != nil {
207 return nil, fmt.Errorf("failed to create logger: %w", err)
208 }
209
210 // Connect to database
211 db, err := NewDatabase(cfg.DatabaseURL, cfg.MaxDBConnections)
212 if err != nil {
213 return nil, fmt.Errorf("failed to connect to database: %w", err)
214 }
215
216 // Initialize services
217 auth := NewAuthService(db, cfg.JWTSecret, cfg.JWTExpiration)
218 email := NewEmailService(cfg.SMTPServer)
219 recomm := NewRecommendationService(db, cfg.RecommendEngine)
220
221 // Create API server
222 server := NewAPIServer(cfg, logger, auth, email, recomm)
223
224 // Create health checker
225 health := NewHealthChecker(db, cfg.Environment, cfg.ServiceName, cfg.ServiceVersion)
226
227 return &Application{
228 config: cfg,
229 server: server,
230 database: db,
231 auth: auth,
232 email: email,
233 recomm: recomm,
234 health: health,
235 logger: logger,
236 }, nil
237}
238
239func (app *Application) Start(ctx context.Context) error {
240 app.logger.Info("Starting application: %s v%s", app.config.ServiceName, app.config.ServiceVersion)
241 app.logger.Info("Environment: %s", app.config.Environment)
242 app.logger.Info("Port: %s", app.config.Port)
243
244 // Run database migration
245 go func() {
246 if err := app.database.Migrate(ctx); err != nil {
247 app.logger.Error("Database migration failed: %v", err)
248 }
249 }()
250
251 // Start recommendation engine
252 if err := app.recomm.Start(ctx); err != nil {
253 return fmt.Errorf("failed to start recommendation engine: %w", err)
254 }
255
256 // Start health check server
257 go func() {
258 app.logger.Info("Starting health check server on port %s", app.config.HealthPort)
259 if err := app.health.Start(ctx, app.config.HealthPort); err != nil {
260 app.logger.Error("Health check server failed: %v", err)
261 }
262 }()
263
264 // Start main API server
265 app.logger.Info("Starting API server on port %s", app.config.Port)
266 return app.server.Start(ctx)
267}
268
269func (app *Application) Shutdown(ctx context.Context) error {
270 app.logger.Info("Shutting down application")
271
272 shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
273 defer cancel()
274
275 var wg sync.WaitGroup
276
277 // Shutdown server
278 wg.Add(1)
279 go func() {
280 defer wg.Done()
281 if err := app.server.Shutdown(shutdownCtx); err != nil {
282 app.logger.Error("Server shutdown failed: %v", err)
283 }
284 }()
285
286 // Shutdown recommendation engine
287 wg.Add(1)
288 go func() {
289 defer wg.Done()
290 if err := app.recomm.Shutdown(shutdownCtx); err != nil {
291 app.logger.Error("Recommendation engine shutdown failed: %v", err)
292 }
293 }()
294
295 // Close database
296 wg.Add(1)
297 go func() {
298 defer wg.Done()
299 if err := app.database.Close(); err != nil {
300 app.logger.Error("Database close failed: %v", err)
301 }
302 }()
303
304 wg.Wait()
305
306 app.logger.Info("Application shutdown complete")
307 return nil
308}
309
310func main() {
311 // Create application
312 app, err := NewApplication()
313 if err != nil {
314 fmt.Printf("Failed to create application: %v\n", err)
315 os.Exit(1)
316 }
317
318 // Setup graceful shutdown
319 ctx, cancel := context.WithCancel(context.Background())
320 defer cancel()
321
322 // Handle signals
323 sigChan := make(chan os.Signal, 1)
324 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
325
326 // Start application
327 errChan := make(chan error, 1)
328 go func() {
329 if err := app.Start(ctx); err != nil {
330 errChan <- fmt.Errorf("application failed: %w", err)
331 }
332 }()
333
334 // Wait for signal or error
335 select {
336 case sig := <-sigChan:
337 app.logger.Info("Received shutdown signal: %s", sig.String())
338 cancel()
339 case err := <-errChan:
340 app.logger.Error("Application error: %v", err)
341 cancel()
342 case <-time.After(2 * time.Second):
343 // Simulate running for 2 seconds then shutdown
344 fmt.Println("\n=== Simulating graceful shutdown ===")
345 cancel()
346 }
347
348 // Graceful shutdown
349 if err := app.Shutdown(ctx); err != nil {
350 fmt.Printf("Failed to shutdown application: %v\n", err)
351 os.Exit(1)
352 }
353}
Key Concepts Demonstrated:
- Clean architecture: Clear separation between layers
- Dependency injection: Services composed through constructors
- Graceful shutdown: Proper cleanup of all resources
- Error handling: Structured error propagation
- Configuration management: Environment-based configuration
- Observability: Structured logging and health checks
- Scalability: Organized for team development
Practice Exercises
Exercise 1: Basic Package Creation and Usage
Learning Objectives: Create a simple utility package with proper visibility control and basic functionality
Difficulty: Beginner
Real-World Context: Building reusable string manipulation utilities that demonstrate package organization and export control
Task: Create a string utilities package with functions for capitalizing, reversing, counting words, and validating emails. Implement both public and private functions to demonstrate visibility control.
Show Solution
1// run
2package main
3
4import (
5 "fmt"
6 "regexp"
7 "strings"
8 "unicode"
9)
10
11// CapitalizeFirst capitalizes the first character
12func CapitalizeFirst(s string) string {
13 if s == "" {
14 return s
15 }
16 runes := []rune(s)
17 runes[0] = unicode.ToUpper(runes[0])
18 return string(runes)
19}
20
21// Reverse reverses a string
22func Reverse(s string) string {
23 runes := []rune(s)
24 for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
25 runes[i], runes[j] = runes[j], runes[i]
26 }
27 return string(runes)
28}
29
30// WordCount counts words in a string
31func WordCount(s string) int {
32 if s == "" {
33 return 0
34 }
35 return len(strings.Fields(s))
36}
37
38// IsValidEmail validates email format
39func IsValidEmail(email string) bool {
40 if email == "" {
41 return false
42 }
43 emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
44 matched, _ := regexp.MatchString(emailRegex, email)
45 return matched
46}
47
48func main() {
49 text := "hello world"
50
51 fmt.Printf("Original: %s\n", text)
52 fmt.Printf("Capitalized: %s\n", CapitalizeFirst(text))
53 fmt.Printf("Reversed: %s\n", Reverse(text))
54 fmt.Printf("Word count: %d\n", WordCount(text))
55
56 fmt.Println("\n=== Email Validation ===")
57 emails := []string{
58 "test@example.com",
59 "invalid-email",
60 "user@domain.co.uk",
61 "@nodomain.com",
62 }
63
64 for _, email := range emails {
65 fmt.Printf("%s: %t\n", email, IsValidEmail(email))
66 }
67}
Exercise 2: Package with Structs and Method Receivers
Learning Objectives: Build a configuration package demonstrating proper struct design, method receivers, and validation
Difficulty: Intermediate
Real-World Context: Creating a type-safe configuration system that loads settings from environment with validation
Task: Create a Config struct with private fields and public methods. Implement LoadFromEnv() that validates all settings. Add methods for accessing and modifying configuration safely.
Show Solution
1// run
2package main
3
4import (
5 "fmt"
6 "regexp"
7 "strconv"
8 "time"
9)
10
11// Config holds application configuration
12type Config struct {
13 databaseURL string
14 serverPort string
15 jwtSecret string
16 debug bool
17 logLevel string
18 timeout int
19}
20
21// LoadFromEnv simulates loading from environment
22func LoadFromEnv() (*Config, error) {
23 cfg := &Config{
24 databaseURL: "postgres://localhost:5432/mydb",
25 serverPort: "8080",
26 jwtSecret: "my-secret-jwt-key-123",
27 debug: true,
28 logLevel: "info",
29 timeout: 30,
30 }
31
32 if err := cfg.Validate(); err != nil {
33 return nil, fmt.Errorf("invalid configuration: %w", err)
34 }
35
36 return cfg, nil
37}
38
39// DatabaseURL returns the database URL
40func (c *Config) DatabaseURL() string {
41 return c.databaseURL
42}
43
44// ServerPort returns the server port
45func (c *Config) ServerPort() string {
46 return c.serverPort
47}
48
49// JWTSecret returns the JWT secret
50func (c *Config) JWTSecret() string {
51 return c.jwtSecret
52}
53
54// IsDebug returns debug mode status
55func (c *Config) IsDebug() bool {
56 return c.debug
57}
58
59// LogLevel returns log level
60func (c *Config) LogLevel() string {
61 return c.logLevel
62}
63
64// Timeout returns timeout duration
65func (c *Config) Timeout() time.Duration {
66 return time.Duration(c.timeout) * time.Second
67}
68
69// SetDatabaseURL updates database URL with validation
70func (c *Config) SetDatabaseURL(url string) error {
71 if !isValidDatabaseURL(url) {
72 return fmt.Errorf("invalid database URL format")
73 }
74 c.databaseURL = url
75 return nil
76}
77
78// Validate validates the configuration
79func (c *Config) Validate() error {
80 if c.databaseURL == "" {
81 return fmt.Errorf("database URL is required")
82 }
83
84 if !isValidDatabaseURL(c.databaseURL) {
85 return fmt.Errorf("invalid database URL format")
86 }
87
88 if c.serverPort == "" {
89 return fmt.Errorf("server port is required")
90 }
91
92 if !isValidPort(c.serverPort) {
93 return fmt.Errorf("invalid server port: %s", c.serverPort)
94 }
95
96 if c.jwtSecret == "" {
97 return fmt.Errorf("JWT secret is required")
98 }
99
100 if len(c.jwtSecret) < 16 {
101 return fmt.Errorf("JWT secret must be at least 16 characters")
102 }
103
104 return nil
105}
106
107// isValidDatabaseURL validates database URL (private)
108func isValidDatabaseURL(url string) bool {
109 postgresPattern := `^postgres://.*`
110 mysqlPattern := `^mysql://.*`
111
112 postgresMatch, _ := regexp.MatchString(postgresPattern, url)
113 mysqlMatch, _ := regexp.MatchString(mysqlPattern, url)
114
115 return postgresMatch || mysqlMatch
116}
117
118// isValidPort validates port number (private)
119func isValidPort(port string) bool {
120 portNum, err := strconv.Atoi(port)
121 if err != nil {
122 return false
123 }
124 return portNum >= 1 && portNum <= 65535
125}
126
127func main() {
128 // Load configuration
129 cfg, err := LoadFromEnv()
130 if err != nil {
131 fmt.Printf("Error loading config: %v\n", err)
132 return
133 }
134
135 fmt.Println("=== Configuration Loaded ===")
136 fmt.Printf("Database URL: %s\n", cfg.DatabaseURL())
137 fmt.Printf("Server Port: %s\n", cfg.ServerPort())
138 fmt.Printf("Debug Mode: %t\n", cfg.IsDebug())
139 fmt.Printf("Log Level: %s\n", cfg.LogLevel())
140 fmt.Printf("Timeout: %v\n", cfg.Timeout())
141 fmt.Printf("JWT Secret: [REDACTED]\n")
142
143 // Test validation
144 fmt.Println("\n=== Testing Validation ===")
145 testCfg := &Config{
146 databaseURL: "invalid-url",
147 serverPort: "8080",
148 jwtSecret: "short",
149 }
150
151 if err := testCfg.Validate(); err != nil {
152 fmt.Printf("Validation error (expected): %v\n", err)
153 }
154
155 // Test update method
156 fmt.Println("\n=== Testing Update Method ===")
157 if err := cfg.SetDatabaseURL("postgres://newhost:5432/newdb"); err != nil {
158 fmt.Printf("Error: %v\n", err)
159 } else {
160 fmt.Printf("Updated database URL: %s\n", cfg.DatabaseURL())
161 }
162}
Exercise 3: Multi-Package Application with Interfaces
Learning Objectives: Create a complete mini-application with multiple packages using interfaces for clean separation of concerns
Difficulty: Advanced
Real-World Context: Building a layered application architecture with repositories, services, and HTTP handlers
Task: Create models, database repository, service layer, and API handlers. Use interfaces to enable dependency injection and testability.
Show Solution
1// run
2package main
3
4import (
5 "fmt"
6 "time"
7)
8
9// User model
10type User struct {
11 ID int
12 Email string
13 Name string
14 CreatedAt time.Time
15}
16
17// Product model
18type Product struct {
19 ID int
20 Name string
21 Price float64
22}
23
24// UserRepository interface
25type UserRepository interface {
26 Create(user *User) error
27 FindByID(id int) (*User, error)
28 FindByEmail(email string) (*User, error)
29 Update(user *User) error
30 Delete(id int) error
31}
32
33// ProductRepository interface
34type ProductRepository interface {
35 Create(product *Product) error
36 FindByID(id int) (*Product, error)
37 List() ([]*Product, error)
38}
39
40// SQLUserRepository implements UserRepository
41type SQLUserRepository struct {
42 users map[int]*User
43 nextID int
44}
45
46func NewUserRepository() UserRepository {
47 return &SQLUserRepository{
48 users: make(map[int]*User),
49 nextID: 1,
50 }
51}
52
53func (r *SQLUserRepository) Create(user *User) error {
54 user.ID = r.nextID
55 user.CreatedAt = time.Now()
56 r.users[user.ID] = user
57 r.nextID++
58 return nil
59}
60
61func (r *SQLUserRepository) FindByID(id int) (*User, error) {
62 user, exists := r.users[id]
63 if !exists {
64 return nil, fmt.Errorf("user not found")
65 }
66 return user, nil
67}
68
69func (r *SQLUserRepository) FindByEmail(email string) (*User, error) {
70 for _, user := range r.users {
71 if user.Email == email {
72 return user, nil
73 }
74 }
75 return nil, fmt.Errorf("user not found")
76}
77
78func (r *SQLUserRepository) Update(user *User) error {
79 if _, exists := r.users[user.ID]; !exists {
80 return fmt.Errorf("user not found")
81 }
82 r.users[user.ID] = user
83 return nil
84}
85
86func (r *SQLUserRepository) Delete(id int) error {
87 delete(r.users, id)
88 return nil
89}
90
91// SQLProductRepository implements ProductRepository
92type SQLProductRepository struct {
93 products map[int]*Product
94 nextID int
95}
96
97func NewProductRepository() ProductRepository {
98 return &SQLProductRepository{
99 products: make(map[int]*Product),
100 nextID: 1,
101 }
102}
103
104func (r *SQLProductRepository) Create(product *Product) error {
105 product.ID = r.nextID
106 r.products[product.ID] = product
107 r.nextID++
108 return nil
109}
110
111func (r *SQLProductRepository) FindByID(id int) (*Product, error) {
112 product, exists := r.products[id]
113 if !exists {
114 return nil, fmt.Errorf("product not found")
115 }
116 return product, nil
117}
118
119func (r *SQLProductRepository) List() ([]*Product, error) {
120 products := make([]*Product, 0, len(r.products))
121 for _, p := range r.products {
122 products = append(products, p)
123 }
124 return products, nil
125}
126
127// UserService provides business logic
128type UserService struct {
129 userRepo UserRepository
130}
131
132func NewUserService(repo UserRepository) *UserService {
133 return &UserService{userRepo: repo}
134}
135
136func (s *UserService) CreateUser(name, email string) (*User, error) {
137 if name == "" || email == "" {
138 return nil, fmt.Errorf("name and email are required")
139 }
140
141 // Check if user exists
142 if existing, _ := s.userRepo.FindByEmail(email); existing != nil {
143 return nil, fmt.Errorf("user with email %s already exists", email)
144 }
145
146 user := &User{
147 Name: name,
148 Email: email,
149 }
150
151 if err := s.userRepo.Create(user); err != nil {
152 return nil, fmt.Errorf("failed to create user: %w", err)
153 }
154
155 return user, nil
156}
157
158func (s *UserService) GetUser(id int) (*User, error) {
159 return s.userRepo.FindByID(id)
160}
161
162// ProductService provides business logic
163type ProductService struct {
164 productRepo ProductRepository
165}
166
167func NewProductService(repo ProductRepository) *ProductService {
168 return &ProductService{productRepo: repo}
169}
170
171func (s *ProductService) CreateProduct(name string, price float64) (*Product, error) {
172 if name == "" {
173 return nil, fmt.Errorf("name is required")
174 }
175 if price <= 0 {
176 return nil, fmt.Errorf("price must be positive")
177 }
178
179 product := &Product{
180 Name: name,
181 Price: price,
182 }
183
184 if err := s.productRepo.Create(product); err != nil {
185 return nil, fmt.Errorf("failed to create product: %w", err)
186 }
187
188 return product, nil
189}
190
191func (s *ProductService) ListProducts() ([]*Product, error) {
192 return s.productRepo.List()
193}
194
195// APIServer simulates HTTP server
196type APIServer struct {
197 port string
198 userService *UserService
199 productService *ProductService
200}
201
202func NewAPIServer(port string, userSvc *UserService, productSvc *ProductService) *APIServer {
203 return &APIServer{
204 port: port,
205 userService: userSvc,
206 productService: productSvc,
207 }
208}
209
210func (s *APIServer) PrintRoutes() {
211 routes := []string{
212 "POST /api/users - Create user",
213 "GET /api/users/:id - Get user",
214 "POST /api/products - Create product",
215 "GET /api/products - List products",
216 }
217 fmt.Println("Available endpoints:")
218 for _, route := range routes {
219 fmt.Printf(" %s\n", route)
220 }
221}
222
223func (s *APIServer) Start() error {
224 fmt.Printf("Server would start on port %s\n", s.port)
225 return nil
226}
227
228func main() {
229 fmt.Println("=== Initializing Application ===")
230
231 // Create repositories
232 userRepo := NewUserRepository()
233 productRepo := NewProductRepository()
234
235 // Create services
236 userService := NewUserService(userRepo)
237 productService := NewProductService(productRepo)
238
239 // Create API server
240 server := NewAPIServer("8080", userService, productService)
241
242 fmt.Println("\n=== Creating Test Data ===")
243
244 // Create users
245 user1, _ := userService.CreateUser("Alice", "alice@example.com")
246 user2, _ := userService.CreateUser("Bob", "bob@example.com")
247 fmt.Printf("Created users: %+v, %+v\n", user1, user2)
248
249 // Create products
250 product1, _ := productService.CreateProduct("Laptop", 999.99)
251 product2, _ := productService.CreateProduct("Mouse", 29.99)
252 fmt.Printf("Created products: %+v, %+v\n", product1, product2)
253
254 // List all products
255 fmt.Println("\n=== Listing All Products ===")
256 products, _ := productService.ListProducts()
257 for _, p := range products {
258 fmt.Printf("Product: %s - $%.2f\n", p.Name, p.Price)
259 }
260
261 fmt.Println("\n=== Server Configuration ===")
262 server.PrintRoutes()
263 server.Start()
264}
Exercise 4: Package Organization Patterns
Learning Objectives: Apply domain-driven design principles to organize a realistic application
Difficulty: Advanced
Real-World Context: Structuring a blogging platform with users, posts, comments, and authentication
Task: Design and implement a multi-domain application structure. Create separate packages for user management, post management, comment management, and authentication. Demonstrate clean boundaries and dependencies.
Show Solution
1// run
2package main
3
4import (
5 "fmt"
6 "time"
7)
8
9// User domain
10type User struct {
11 ID int
12 Username string
13 Email string
14 CreatedAt time.Time
15}
16
17type UserService struct {
18 users map[int]*User
19 nextID int
20}
21
22func NewUserService() *UserService {
23 return &UserService{
24 users: make(map[int]*User),
25 nextID: 1,
26 }
27}
28
29func (s *UserService) CreateUser(username, email string) (*User, error) {
30 user := &User{
31 ID: s.nextID,
32 Username: username,
33 Email: email,
34 CreatedAt: time.Now(),
35 }
36 s.users[user.ID] = user
37 s.nextID++
38 return user, nil
39}
40
41func (s *UserService) GetUser(id int) (*User, error) {
42 user, exists := s.users[id]
43 if !exists {
44 return nil, fmt.Errorf("user not found")
45 }
46 return user, nil
47}
48
49// Post domain
50type Post struct {
51 ID int
52 UserID int
53 Title string
54 Content string
55 CreatedAt time.Time
56}
57
58type PostService struct {
59 posts map[int]*Post
60 nextID int
61 userService *UserService
62}
63
64func NewPostService(userSvc *UserService) *PostService {
65 return &PostService{
66 posts: make(map[int]*Post),
67 nextID: 1,
68 userService: userSvc,
69 }
70}
71
72func (s *PostService) CreatePost(userID int, title, content string) (*Post, error) {
73 // Verify user exists
74 if _, err := s.userService.GetUser(userID); err != nil {
75 return nil, fmt.Errorf("invalid user: %w", err)
76 }
77
78 post := &Post{
79 ID: s.nextID,
80 UserID: userID,
81 Title: title,
82 Content: content,
83 CreatedAt: time.Now(),
84 }
85 s.posts[post.ID] = post
86 s.nextID++
87 return post, nil
88}
89
90func (s *PostService) GetPost(id int) (*Post, error) {
91 post, exists := s.posts[id]
92 if !exists {
93 return nil, fmt.Errorf("post not found")
94 }
95 return post, nil
96}
97
98func (s *PostService) ListUserPosts(userID int) ([]*Post, error) {
99 var posts []*Post
100 for _, post := range s.posts {
101 if post.UserID == userID {
102 posts = append(posts, post)
103 }
104 }
105 return posts, nil
106}
107
108// Comment domain
109type Comment struct {
110 ID int
111 PostID int
112 UserID int
113 Content string
114 CreatedAt time.Time
115}
116
117type CommentService struct {
118 comments map[int]*Comment
119 nextID int
120 postService *PostService
121 userService *UserService
122}
123
124func NewCommentService(postSvc *PostService, userSvc *UserService) *CommentService {
125 return &CommentService{
126 comments: make(map[int]*Comment),
127 nextID: 1,
128 postService: postSvc,
129 userService: userSvc,
130 }
131}
132
133func (s *CommentService) CreateComment(postID, userID int, content string) (*Comment, error) {
134 // Verify post exists
135 if _, err := s.postService.GetPost(postID); err != nil {
136 return nil, fmt.Errorf("invalid post: %w", err)
137 }
138
139 // Verify user exists
140 if _, err := s.userService.GetUser(userID); err != nil {
141 return nil, fmt.Errorf("invalid user: %w", err)
142 }
143
144 comment := &Comment{
145 ID: s.nextID,
146 PostID: postID,
147 UserID: userID,
148 Content: content,
149 CreatedAt: time.Now(),
150 }
151 s.comments[comment.ID] = comment
152 s.nextID++
153 return comment, nil
154}
155
156func (s *CommentService) ListPostComments(postID int) ([]*Comment, error) {
157 var comments []*Comment
158 for _, comment := range s.comments {
159 if comment.PostID == postID {
160 comments = append(comments, comment)
161 }
162 }
163 return comments, nil
164}
165
166// Auth domain
167type AuthService struct {
168 userService *UserService
169 sessions map[string]int // token -> userID
170}
171
172func NewAuthService(userSvc *UserService) *AuthService {
173 return &AuthService{
174 userService: userSvc,
175 sessions: make(map[string]int),
176 }
177}
178
179func (s *AuthService) Login(email string) (string, error) {
180 // Simplified: find user by email
181 for _, user := range s.userService.users {
182 if user.Email == email {
183 token := fmt.Sprintf("token_%d_%d", user.ID, time.Now().Unix())
184 s.sessions[token] = user.ID
185 return token, nil
186 }
187 }
188 return "", fmt.Errorf("authentication failed")
189}
190
191func (s *AuthService) ValidateToken(token string) (int, error) {
192 userID, exists := s.sessions[token]
193 if !exists {
194 return 0, fmt.Errorf("invalid token")
195 }
196 return userID, nil
197}
198
199func main() {
200 fmt.Println("=== Blogging Platform Demo ===")
201
202 // Initialize services
203 userSvc := NewUserService()
204 authSvc := NewAuthService(userSvc)
205 postSvc := NewPostService(userSvc)
206 commentSvc := NewCommentService(postSvc, userSvc)
207
208 // Create users
209 fmt.Println("\n1. Creating Users")
210 alice, _ := userSvc.CreateUser("alice", "alice@example.com")
211 bob, _ := userSvc.CreateUser("bob", "bob@example.com")
212 fmt.Printf("Created users: %s, %s\n", alice.Username, bob.Username)
213
214 // Authenticate user
215 fmt.Println("\n2. User Authentication")
216 token, _ := authSvc.Login("alice@example.com")
217 fmt.Printf("Alice logged in with token: %s\n", token)
218
219 // Validate token
220 userID, _ := authSvc.ValidateToken(token)
221 fmt.Printf("Token validated for user ID: %d\n", userID)
222
223 // Create posts
224 fmt.Println("\n3. Creating Posts")
225 post1, _ := postSvc.CreatePost(alice.ID, "My First Post", "Hello world!")
226 post2, _ := postSvc.CreatePost(alice.ID, "Go Programming", "Learning Go is fun!")
227 post3, _ := postSvc.CreatePost(bob.ID, "Bob's Post", "Nice platform!")
228 fmt.Printf("Created %d posts\n", 3)
229
230 // List user posts
231 fmt.Println("\n4. Listing Alice's Posts")
232 alicePosts, _ := postSvc.ListUserPosts(alice.ID)
233 for _, post := range alicePosts {
234 fmt.Printf(" - %s: %s\n", post.Title, post.Content)
235 }
236
237 // Create comments
238 fmt.Println("\n5. Creating Comments")
239 commentSvc.CreateComment(post1.ID, bob.ID, "Great first post!")
240 commentSvc.CreateComment(post1.ID, bob.ID, "Looking forward to more!")
241 commentSvc.CreateComment(post2.ID, alice.ID, "Thanks everyone!")
242
243 // List comments
244 fmt.Println("\n6. Listing Comments on First Post")
245 comments, _ := commentSvc.ListPostComments(post1.ID)
246 for _, comment := range comments {
247 user, _ := userSvc.GetUser(comment.UserID)
248 fmt.Printf(" - %s: %s\n", user.Username, comment.Content)
249 }
250
251 // Summary
252 fmt.Println("\n=== Summary ===")
253 fmt.Printf("Total Users: %d\n", len(userSvc.users))
254 fmt.Printf("Total Posts: %d\n", len(postSvc.posts))
255 fmt.Printf("Total Comments: %d\n", len(commentSvc.comments))
256 fmt.Printf("Active Sessions: %d\n", len(authSvc.sessions))
257}
Exercise 5: Advanced Package Design with Internal Directory
Learning Objectives: Master the use of internal packages for encapsulation and clean API boundaries
Difficulty: Advanced
Real-World Context: Building a library with public API and private implementation details
Task: Create a monitoring library with public API in root package and private implementation in internal/. Demonstrate how internal/ prevents external packages from importing implementation details.
Show Solution
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "time"
8)
9
10// Metric represents a monitoring metric
11type Metric struct {
12 Name string
13 Value float64
14 Timestamp time.Time
15 Tags map[string]string
16}
17
18// MetricType represents different metric types
19type MetricType int
20
21const (
22 Counter MetricType = iota
23 Gauge
24 Histogram
25)
26
27// Internal collector (would be in internal/ directory)
28type metricCollector struct {
29 mu sync.RWMutex
30 metrics map[string]*Metric
31}
32
33func newMetricCollector() *metricCollector {
34 return &metricCollector{
35 metrics: make(map[string]*Metric),
36 }
37}
38
39func (c *metricCollector) record(metric *Metric) {
40 c.mu.Lock()
41 defer c.mu.Unlock()
42 c.metrics[metric.Name] = metric
43}
44
45func (c *metricCollector) get(name string) (*Metric, bool) {
46 c.mu.RLock()
47 defer c.mu.RUnlock()
48 m, exists := c.metrics[name]
49 return m, exists
50}
51
52func (c *metricCollector) getAll() []*Metric {
53 c.mu.RLock()
54 defer c.mu.RUnlock()
55
56 metrics := make([]*Metric, 0, len(c.metrics))
57 for _, m := range c.metrics {
58 metrics = append(metrics, m)
59 }
60 return metrics
61}
62
63// Internal aggregator (would be in internal/ directory)
64type metricAggregator struct {
65 collector *metricCollector
66}
67
68func newMetricAggregator(collector *metricCollector) *metricAggregator {
69 return &metricAggregator{collector: collector}
70}
71
72func (a *metricAggregator) aggregate() map[string]float64 {
73 metrics := a.collector.getAll()
74 aggregated := make(map[string]float64)
75
76 for _, m := range metrics {
77 aggregated[m.Name] += m.Value
78 }
79
80 return aggregated
81}
82
83// Monitor is the public API (would be in root package)
84type Monitor struct {
85 collector *metricCollector
86 aggregator *metricAggregator
87 name string
88}
89
90// NewMonitor creates a new monitoring instance
91func NewMonitor(name string) *Monitor {
92 collector := newMetricCollector()
93 aggregator := newMetricAggregator(collector)
94
95 return &Monitor{
96 collector: collector,
97 aggregator: aggregator,
98 name: name,
99 }
100}
101
102// RecordCounter records a counter metric
103func (m *Monitor) RecordCounter(name string, value float64, tags map[string]string) {
104 metric := &Metric{
105 Name: name,
106 Value: value,
107 Timestamp: time.Now(),
108 Tags: tags,
109 }
110 m.collector.record(metric)
111}
112
113// RecordGauge records a gauge metric
114func (m *Monitor) RecordGauge(name string, value float64, tags map[string]string) {
115 metric := &Metric{
116 Name: name,
117 Value: value,
118 Timestamp: time.Now(),
119 Tags: tags,
120 }
121 m.collector.record(metric)
122}
123
124// GetMetric retrieves a specific metric
125func (m *Monitor) GetMetric(name string) (*Metric, error) {
126 metric, exists := m.collector.get(name)
127 if !exists {
128 return nil, fmt.Errorf("metric %s not found", name)
129 }
130 return metric, nil
131}
132
133// GetAllMetrics retrieves all metrics
134func (m *Monitor) GetAllMetrics() []*Metric {
135 return m.collector.getAll()
136}
137
138// GetAggregatedMetrics returns aggregated metric values
139func (m *Monitor) GetAggregatedMetrics() map[string]float64 {
140 return m.aggregator.aggregate()
141}
142
143// ExportMetrics formats metrics for export
144func (m *Monitor) ExportMetrics() string {
145 metrics := m.collector.getAll()
146 result := fmt.Sprintf("=== Metrics Export: %s ===\n", m.name)
147
148 for _, metric := range metrics {
149 result += fmt.Sprintf("%s: %.2f (at %s)\n",
150 metric.Name,
151 metric.Value,
152 metric.Timestamp.Format("15:04:05"),
153 )
154
155 if len(metric.Tags) > 0 {
156 result += " Tags: "
157 for k, v := range metric.Tags {
158 result += fmt.Sprintf("%s=%s ", k, v)
159 }
160 result += "\n"
161 }
162 }
163
164 return result
165}
166
167func main() {
168 fmt.Println("=== Monitoring Library Demo ===")
169
170 // Create monitor instance
171 monitor := NewMonitor("web-server")
172
173 // Record various metrics
174 fmt.Println("\n1. Recording Metrics")
175 monitor.RecordCounter("requests_total", 100, map[string]string{
176 "method": "GET",
177 "path": "/api/users",
178 })
179
180 monitor.RecordCounter("requests_total", 50, map[string]string{
181 "method": "POST",
182 "path": "/api/users",
183 })
184
185 monitor.RecordGauge("memory_usage_mb", 256.5, map[string]string{
186 "type": "heap",
187 })
188
189 monitor.RecordGauge("cpu_usage_percent", 45.2, map[string]string{
190 "core": "0",
191 })
192
193 monitor.RecordCounter("errors_total", 5, map[string]string{
194 "type": "validation",
195 })
196
197 // Retrieve specific metric
198 fmt.Println("\n2. Retrieving Specific Metric")
199 if metric, err := monitor.GetMetric("memory_usage_mb"); err == nil {
200 fmt.Printf("Memory Usage: %.2f MB\n", metric.Value)
201 }
202
203 // Get all metrics
204 fmt.Println("\n3. Listing All Metrics")
205 allMetrics := monitor.GetAllMetrics()
206 fmt.Printf("Total metrics recorded: %d\n", len(allMetrics))
207
208 // Get aggregated metrics
209 fmt.Println("\n4. Aggregated Metrics")
210 aggregated := monitor.GetAggregatedMetrics()
211 for name, value := range aggregated {
212 fmt.Printf("%s: %.2f\n", name, value)
213 }
214
215 // Export metrics
216 fmt.Println("\n5. Exporting Metrics")
217 exported := monitor.ExportMetrics()
218 fmt.Print(exported)
219
220 // Demonstrate that internal implementation is hidden
221 fmt.Println("\n=== Key Design Points ===")
222 fmt.Println("- Public API: Monitor struct with public methods")
223 fmt.Println("- Private implementation: metricCollector and metricAggregator")
224 fmt.Println("- Clean boundaries: Users cannot access internal details")
225 fmt.Println("- Thread-safe: Internal synchronization with mutexes")
226 fmt.Println("- Testable: Each component can be tested independently")
227}
Summary
Key Takeaways
Mastered Core Concepts:
- Package Philosophy: Understand Go's simple yet powerful approach to code organization
- Visibility Control: Master capitalization-based public/private access
- Import Management: Organize dependencies efficiently and avoid circular imports
- Package Design: Create focused, reusable, and maintainable packages
- Project Structure: Apply proven patterns for large applications
- Best Practices: Follow Go naming conventions and documentation standards
Real-World Benefits:
- Team Collaboration: Different teams can work on different packages independently
- Code Reusability: Share packages across projects and organizations
- Maintainability: Find and fix issues quickly with well-organized code
- Testing: Test individual packages in isolation with clear boundaries
- Scalability: Organize codebases that grow gracefully with complexity
Critical Safety Rules:
- One package per directory - Go enforces this at compile time
- Use lowercase package names - Follow Go conventions
- Export intentionally - Only make public what's needed externally
- Avoid circular imports - They're caught at compile time but prevent good design
- Prefer domain-driven organization - Group by business capability, not technical layer
- Use
internal/for private code - Prevent external access to implementation details
Decision Matrix
| Situation | Package Structure | Reason |
|---|---|---|
| Small utility functions | Flat with utils/ package |
Simple, focused functionality |
| Web application | Domain-driven with internal/ |
Clear boundaries, team collaboration |
| Library/framework | Layered with clear APIs | Reusable components |
| Microservice | Domain-driven per service | Independent deployment |
| Large enterprise | Mixed: domains + shared pkg/ |
Balance reusability and encapsulation |
Package Design Principles
- Single Responsibility: Each package has one clear purpose
- Clear Boundaries: Public API is minimal and well-defined
- Domain-Driven: Organize by business capability
- Dependency Direction: Dependencies point inward, not circular
- Testable Design: Each package can be tested independently
- Documentation: Public APIs are well-documented
Organization Patterns
Small Projects:
project/
├── main.go
├── utils/
└── config/
Medium Applications:
project/
├── cmd/
├── internal/
│ ├── handlers/
│ ├── models/
│ └── services/
└── pkg/
├── config/
└── logger/
Large Systems:
project/
├── cmd/
├── internal/
│ ├── user/
│ ├── order/
│ ├── payment/
│ └── shipping/
├── pkg/
│ ├── database/
│ ├── cache/
│ └── messaging/
└── api/
Next Steps in Your Go Journey
Now that you've mastered packages, you're ready for:
- Go Modules: Learn dependency management and versioning with
go.mod - Testing: Master unit testing patterns for package-based code
- Build Systems: Understand Go's build tags and cross-compilation
- Architecture: Design larger systems using package principles
- Standard Library: Deep dive into Go's extensive standard library packages
Remember: Packages are your foundation for building maintainable Go applications. Well-designed packages make the difference between code that's a pleasure to work with and code that becomes a nightmare.
Master packages, and you'll build Go applications that scale naturally instead of collapsing under their own complexity.