Best Practices
Guidelines and recommendations for building with Goca.
Code Organization
Keep Layers Independent
Each layer should only depend on interfaces, not concrete implementations.
** Good:**
type UserService struct {
repo repository.UserRepository // Interface
}** Bad:**
type UserService struct {
repo *repository.PostgresUserRepository // Concrete
}Use Dependency Injection
Let the DI container wire dependencies:
func NewUserService(repo repository.UserRepository) usecase.UserService {
return &userService{repo: repo}
}Single Responsibility
Each struct should have one clear purpose:
// One responsibility: handle HTTP requests
type UserHandler struct {
service usecase.UserService
}
// Too many responsibilities
type UserHandler struct {
db *gorm.DB
cache *redis.Client
mailer *smtp.Client
}Error Handling
Use Domain Errors
Define errors in the domain layer:
// internal/domain/errors.go
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidEmail = errors.New("invalid email format")
)Wrap Errors with Context
func (s *userService) GetUser(ctx context.Context, id uint) (*UserResponse, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get user %d: %w", id, err)
}
return toUserResponse(user), nil
}Handle Errors at HTTP Layer
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetByID(r.Context(), id)
if errors.Is(err, domain.ErrUserNotFound) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}Testing
Use Table-Driven Tests
func TestUserService_CreateUser(t *testing.T) {
tests := []struct {
name string
input CreateUserInput
wantErr bool
}{
{
name: "valid user",
input: CreateUserInput{
Name: "John Doe",
Email: "john@example.com",
},
wantErr: false,
},
{
name: "invalid email",
input: CreateUserInput{
Name: "Jane Doe",
Email: "invalid-email",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test implementation
})
}
}Mock External Dependencies
type MockUserRepository struct {
SaveFunc func(ctx context.Context, user *domain.User) error
FindByIDFunc func(ctx context.Context, id uint) (*domain.User, error)
}
func (m *MockUserRepository) Save(ctx context.Context, user *domain.User) error {
return m.SaveFunc(ctx, user)
}Database Operations
Use Transactions
Repository interfaces include SaveWithTx, UpdateWithTx, and DeleteWithTx methods. Use them when you need atomic operations across multiple entities:
func (s *orderService) CreateOrder(ctx context.Context, input CreateOrderInput) error {
order := domain.Order{...}
if err := s.repo.SaveWithTx(ctx, nil, &order); err != nil {
return err
}
return nil
}Use Context
Always pass context for cancellation and timeouts:
func (r *PostgresUserRepository) FindByID(ctx context.Context, id uint) (*domain.User, error) {
var user domain.User
err := r.db.WithContext(ctx).First(&user, id).Error
return &user, err
}API Design
Use DTOs
Use input/output DTOs in use cases instead of exposing domain entities directly in handler responses. Note: the generated use case interfaces currently return domain entities directly; consider wrapping in DTOs for API boundaries.
// Use DTOs
type CreateUserInput struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
type UserResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}Consistent Response Format
Handlers return data directly with appropriate HTTP status codes. Consider wrapping responses in a standard envelope (e.g., {"data": ..., "error": ...}) for API consistency — this is not generated by Goca and should be added manually.
Security
Validate Input
type CreateUserInput struct {
Name string `json:"name" binding:"required,min=2,max=100"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
}Don't Log Sensitive Data
// Bad - logs password
log.Printf("Creating user: %+v", input)
// Good - excludes sensitive fields
log.Printf("Creating user: name=%s, email=%s", input.Name, input.Email)Performance
Use Pagination
type ListUsersInput struct {
Page int `form:"page" binding:"min=1"`
PageSize int `form:"page_size" binding:"min=1,max=100"`
}
func (r *PostgresUserRepository) FindAll(ctx context.Context, page, pageSize int) ([]*domain.User, error) {
var users []*domain.User
offset := (page - 1) * pageSize
err := r.db.WithContext(ctx).
Offset(offset).
Limit(pageSize).
Find(&users).Error
return users, err
}Use Indexes
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"unique;index"` // Add index for lookups
Name string `gorm:"index"` // Index for searches
}See Also
- Project Structure - Directory organization
- Clean Architecture - Architecture principles