6.6 KiB
6.6 KiB
Code Guidelines for WhatsHooked
General Principles
- Write clean, idiomatic Go code following standard conventions
- Use meaningful variable and function names
- Keep functions small and focused on a single responsibility
- Document exported functions, types, and packages
- Handle errors explicitly, never ignore them
- Use context.Context for cancellation and timeout handling
Project Structure
whatshooked/
├── cmd/ # Command-line entry points
│ ├── server/ # HTTP server command
│ └── cli/ # CLI tools
├── pkg/ # Public packages
│ ├── auth/ # Authentication & authorization
│ ├── api/ # API endpoints
│ ├── cache/ # Caching layer
│ ├── config/ # Configuration management
│ ├── events/ # Event system
│ ├── eventlogger/ # Event logging
│ ├── handlers/ # HTTP handlers
│ ├── hooks/ # Webhook management
│ ├── logging/ # Structured logging
│ ├── storage/ # Database storage layer
│ ├── utils/ # Utility functions
│ ├── webserver/ # Web server with ResolveSpec
│ ├── whatsapp/ # WhatsApp integration
│ └── whatshooked/ # Core application logic
└── tooldoc/ # Tool & library documentation
Naming Conventions
Packages
- Use lowercase, single-word package names
- Use descriptive names that reflect the package's purpose
- Avoid generic names like
utilorcommon(useutilswith specific subdirectories if needed)
Files
- Use snake_case for file names:
user_service.go,auth_middleware.go - Test files:
user_service_test.go - Keep related functionality in the same file
Variables & Functions
- Use camelCase for private:
userService,handleRequest - Use PascalCase for exported:
UserService,HandleRequest - Use descriptive names: prefer
userRepositoryoverur - Boolean variables should be prefixed:
isValid,hasPermission,canAccess
Constants
- Use PascalCase for exported:
DefaultTimeout - Use camelCase for private:
defaultTimeout - Group related constants together
Interfaces
- Name interfaces with -er suffix when appropriate:
Reader,Writer,Handler - Use descriptive names:
UserRepository,AuthService
Error Handling
// Wrap errors with context
if err != nil {
return fmt.Errorf("failed to load user: %w", err)
}
// Check specific errors
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
// Custom error types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
Logging
Use zerolog for structured logging:
import "github.com/rs/zerolog/log"
log.Info().
Str("user_id", userID).
Msg("User logged in")
log.Error().
Err(err).
Str("operation", "database_query").
Msg("Failed to query database")
Context Usage
Always pass context as the first parameter:
func ProcessRequest(ctx context.Context, userID string) error {
// Check for cancellation
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Pass context to downstream calls
user, err := userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
return nil
}
Testing
- Write table-driven tests
- Use descriptive test names:
TestUserService_Create_WithValidData_Success - Mock external dependencies
- Aim for high coverage of business logic
- Use t.Run for subtests
func TestUserService_Create(t *testing.T) {
tests := []struct {
name string
input *User
wantErr bool
}{
{
name: "valid user",
input: &User{Name: "John", Email: "john@example.com"},
wantErr: false,
},
{
name: "missing email",
input: &User{Name: "John"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
service := NewUserService()
err := service.Create(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Database Operations
- Use transactions for multiple related operations
- Always use prepared statements
- Handle NULL values properly
- Use meaningful struct tags
type User struct {
ID string `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
ResolveSpec Integration
When using ResolveSpec:
- Register all models in the registry
- Use schema.table format: "public.users", "core.accounts"
- Implement hooks for auth and validation
- Use lifecycle hooks for audit logging
API Design
- Use RESTful conventions
- Return appropriate HTTP status codes
- Include meaningful error messages
- Version your APIs:
/api/v1/users - Document all endpoints
Security
- Never log sensitive data (passwords, tokens, etc.)
- Validate all input
- Use parameterized queries
- Implement rate limiting
- Use HTTPS in production
- Sanitize user input
- Implement proper CORS policies
Dependencies
- Keep dependencies minimal
- Pin versions in go.mod
- Regularly update dependencies
- Document why each dependency is needed
Documentation
- Document all exported functions, types, and packages
- Use godoc format
- Include examples in documentation
- Keep README.md up to date
- Document configuration options
Comments
// Package auth provides authentication and authorization functionality.
package auth
// User represents a system user.
type User struct {
ID string
}
// Authenticate verifies user credentials and returns a token.
// Returns ErrInvalidCredentials if authentication fails.
func Authenticate(ctx context.Context, username, password string) (string, error) {
// Implementation
}
Performance
- Use connection pools for databases
- Implement caching where appropriate
- Avoid N+1 queries
- Use batch operations when possible
- Profile critical paths
- Set appropriate timeouts
Git Workflow
- Write clear commit messages
- Keep commits atomic and focused
- Use conventional commits format
- Create feature branches
- Run tests before committing
- Review your own changes before pushing