Initial commit
This commit is contained in:
171
skills/golang-dev-guidelines/reference/golang-core-principles.md
Normal file
171
skills/golang-dev-guidelines/reference/golang-core-principles.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Go Core Principles and Development Guidelines
|
||||
|
||||
This document provides comprehensive Go development standards and best practices based on Go proverbs, SOLID principles, and industry-standard design approaches.
|
||||
|
||||
## Go Proverbs
|
||||
|
||||
Follow these core Go philosophy principles when writing code:
|
||||
|
||||
### Communication and Concurrency
|
||||
|
||||
- **Don't communicate by sharing memory, share memory by communicating**: Use channels to pass data between goroutines instead of shared variables.
|
||||
- **Concurrency is not parallelism**: Concurrency structures code; parallelism executes multiple computations simultaneously.
|
||||
- **Channels orchestrate; mutexes serialize**: Channels coordinate goroutines; mutexes protect shared state access.
|
||||
|
||||
### Design and Abstraction
|
||||
|
||||
- **The bigger the interface, the weaker the abstraction**: Small interfaces with fewer methods are more flexible and powerful. Prefer small, focused interfaces (ideally 1-3 methods).
|
||||
- **Make the zero value useful**: Design types so their zero value is ready to use without initialization.
|
||||
- **interface{} says nothing**: Empty interfaces provide no type information or guarantees about behavior. Use specific types or generic constraints instead.
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **Gofmt's style is no one's favorite, yet gofmt is everyone's favorite**: Consistent formatting matters more than personal style preferences. Always run `gofmt` or use editor integration.
|
||||
- **A little copying is better than a little dependency**: Duplicate small code rather than adding unnecessary external dependencies.
|
||||
- **Clear is better than clever**: Write readable, straightforward code over smart but obscure solutions.
|
||||
- **Reflection is never clear**: Reflection makes code harder to understand and reason about. Avoid unless absolutely necessary.
|
||||
|
||||
### Platform and Safety
|
||||
|
||||
- **Syscall must always be guarded with build tags**: Platform-specific system calls need build constraints for portability.
|
||||
- **Cgo must always be guarded with build tags**: C interop code should be conditionally compiled for platform compatibility.
|
||||
- **Cgo is not Go**: C code integration loses Go's safety, simplicity, and performance guarantees.
|
||||
- **With the unsafe package there are no guarantees**: Unsafe bypasses type safety and memory protection mechanisms. Avoid unless absolutely necessary and document thoroughly.
|
||||
|
||||
### Error Handling and Documentation
|
||||
|
||||
- **Errors are values**: Treat errors as regular values that can be examined and handled.
|
||||
- **Don't just check errors, handle them gracefully**: Add context and appropriate responses when processing errors. Wrap errors with context using `fmt.Errorf("context: %w", err)`.
|
||||
- **Don't panic**: Reserve panic for truly exceptional, unrecoverable situations; prefer returning errors.
|
||||
|
||||
### Architecture and Documentation
|
||||
|
||||
- **Design the architecture, name the components, document the details**: Focus design on structure, naming on clarity, documentation on specifics.
|
||||
- **Documentation is for users**: Write docs explaining how to use code, not implementation details.
|
||||
|
||||
## SOLID Principles
|
||||
|
||||
Apply these software design principles to create maintainable, extensible code:
|
||||
|
||||
### Single Responsibility Principle (SRP)
|
||||
|
||||
Each struct, function, or package should have only one reason to change. Keep responsibilities focused and well-defined.
|
||||
|
||||
**Example**: Separate concerns clearly:
|
||||
|
||||
- HTTP handlers process requests and responses only
|
||||
- Services contain business logic and orchestration
|
||||
- Repositories handle data persistence
|
||||
- Clients manage external API interactions
|
||||
|
||||
### Open/Closed Principle (OCP)
|
||||
|
||||
Software entities should be open for extension but closed for modification. Use interfaces to allow behavior extension without changing existing code.
|
||||
|
||||
**Example**: Define client interfaces that can be implemented differently for testing, mocking, or production environments without modifying consumer code.
|
||||
|
||||
### Liskov Substitution Principle (LSP)
|
||||
|
||||
Subtypes must be substitutable for their base types without breaking functionality. Ensure interface implementations fully honor the contract.
|
||||
|
||||
**Example**: Any implementation of a `Logger` interface should behave correctly when substituted for another implementation, whether it's a file logger, stdout logger, or no-op logger.
|
||||
|
||||
### Interface Segregation Principle (ISP)
|
||||
|
||||
Clients shouldn't depend on interfaces they don't use; prefer specific interfaces. Break large interfaces into smaller, focused ones.
|
||||
|
||||
**Example**: Instead of one large `Storage` interface, create separate focused interfaces:
|
||||
|
||||
```go
|
||||
type Reader interface {
|
||||
Read(ctx context.Context, key string) ([]byte, error)
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(ctx context.Context, key string, data []byte) error
|
||||
}
|
||||
|
||||
type Deleter interface {
|
||||
Delete(ctx context.Context, key string) error
|
||||
}
|
||||
```
|
||||
|
||||
Functions can then depend only on the capabilities they need (e.g., a cache invalidator only needs `Deleter`).
|
||||
|
||||
### Dependency Inversion Principle (DIP)
|
||||
|
||||
Depend on abstractions, not concrete implementations; high-level modules shouldn't depend on low-level modules.
|
||||
|
||||
**Example**: Business logic should depend on repository interfaces, not concrete database implementations. HTTP handlers should depend on service interfaces, not concrete service structs.
|
||||
|
||||
## Additional Design Principles
|
||||
|
||||
### Don't Repeat Yourself (DRY)
|
||||
|
||||
Avoid duplicating logic; abstract common functionality into reusable components. However, remember: "A little copying is better than a little dependency."
|
||||
|
||||
**Balance**: Duplicate simple code rather than creating premature abstractions. Extract when patterns emerge across 3+ locations.
|
||||
|
||||
### You Aren't Gonna Need It (YAGNI)
|
||||
|
||||
Don't add functionality until it's actually needed; avoid premature features. Implement what's required now, not what might be needed later.
|
||||
|
||||
### Keep It Simple, Stupid (KISS)
|
||||
|
||||
Favor simple solutions over complex ones; avoid unnecessary complexity. Choose the straightforward approach unless complexity is justified.
|
||||
|
||||
### Composition over Inheritance
|
||||
|
||||
Build functionality by composing objects rather than using deep inheritance hierarchies. Go naturally encourages this through struct embedding and interfaces.
|
||||
|
||||
**Example**: Compose functionality by embedding specialized components:
|
||||
|
||||
```go
|
||||
// Instead of inheritance, compose capabilities
|
||||
type UserService struct {
|
||||
repo UserRepository
|
||||
cache Cache
|
||||
logger Logger
|
||||
mailer EmailSender
|
||||
}
|
||||
|
||||
// Struct embedding for shared behavior
|
||||
type BaseHandler struct {
|
||||
logger Logger
|
||||
}
|
||||
|
||||
type UserHandler struct {
|
||||
BaseHandler // Embedded for shared logging
|
||||
userService *UserService
|
||||
}
|
||||
```
|
||||
|
||||
## Applying These Guidelines
|
||||
|
||||
### When Writing Code
|
||||
|
||||
1. **Start with interfaces**: Define what behavior is needed before implementing
|
||||
2. **Keep functions small**: Each function should do one thing well
|
||||
3. **Use meaningful names**: Names should reveal intent without needing comments
|
||||
4. **Handle errors explicitly**: Don't ignore errors; add context when wrapping
|
||||
5. **Write tests first (TDD)**: Define expected behavior through tests
|
||||
6. **Refactor continuously**: Improve code structure as understanding evolves
|
||||
7. **Review against proverbs**: Check code against Go proverbs before committing
|
||||
8. **Document public APIs**: Add godoc comments for exported types and functions
|
||||
|
||||
### When Reviewing Code
|
||||
|
||||
1. Check adherence to Go proverbs
|
||||
2. Verify SOLID principles are followed
|
||||
3. Ensure tests cover happy paths and edge cases
|
||||
4. Look for unnecessary complexity
|
||||
5. Validate error handling is graceful with context
|
||||
6. Confirm interfaces are small and focused
|
||||
7. Check that zero values are useful where applicable
|
||||
|
||||
### When Creating Plans or Researching Go Topics
|
||||
|
||||
1. Identify relevant Go proverbs and principles that apply to the topic
|
||||
2. Outline how these guidelines influence design decisions
|
||||
3. Provide examples demonstrating best practices
|
||||
4. Suggest testing strategies aligned with the guidelines
|
||||
@@ -0,0 +1,373 @@
|
||||
# Go Testing Guidelines
|
||||
|
||||
This document provides comprehensive testing standards and best practices for writing maintainable, effective tests in Go.
|
||||
|
||||
## Testing Philosophy
|
||||
|
||||
Write comprehensive, maintainable tests that ensure code quality and prevent regressions. Focus on testing behavior, not implementation details.
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- Write tests that cover both happy paths and edge cases
|
||||
- Test error conditions and boundary values
|
||||
- Aim for meaningful coverage, not just high percentages
|
||||
- Focus on testing behavior, not implementation details
|
||||
|
||||
## Test Organization
|
||||
|
||||
### Table-Driven Tests
|
||||
|
||||
**Use table-driven tests** for better organization and readability. This pattern allows you to:
|
||||
- Group related test cases in a single test function with subtests
|
||||
- Name test cases descriptively to document expected behavior
|
||||
- Easily add new test cases without duplicating test logic
|
||||
- Run specific test cases using `-run` flag
|
||||
|
||||
**Example structure**:
|
||||
|
||||
```go
|
||||
func TestFunctionName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input InputType
|
||||
want OutputType
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "successful case with valid input",
|
||||
input: validInput,
|
||||
want: expectedOutput,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "error case with invalid input",
|
||||
input: invalidInput,
|
||||
want: zeroValue,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := FunctionName(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("FunctionName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Test Naming
|
||||
|
||||
- Test function names should be clear and descriptive: `TestFunctionName`
|
||||
- Subtest names (in table-driven tests) should describe the scenario being tested
|
||||
- Use descriptive names that explain what is being tested and expected outcome
|
||||
|
||||
## Assertions
|
||||
|
||||
### Using Assertion Libraries
|
||||
|
||||
**Use assertion libraries** (like `testify/assert`) for clarity and better error messages:
|
||||
|
||||
```go
|
||||
import "github.com/stretchr/testify/assert"
|
||||
|
||||
assert.NoError(t, err, "operation should not error")
|
||||
assert.Equal(t, expected, actual, "values should match")
|
||||
assert.True(t, condition, "condition should be true")
|
||||
assert.Len(t, slice, 3, "should have exactly 3 elements")
|
||||
assert.NotNil(t, obj, "object should not be nil")
|
||||
assert.Contains(t, slice, item, "slice should contain item")
|
||||
```
|
||||
|
||||
### Without Assertion Libraries
|
||||
|
||||
If not using an assertion library, follow these patterns:
|
||||
|
||||
```go
|
||||
// Check for unexpected errors
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Compare values
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
|
||||
// Check error expectations
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
|
||||
// Verify conditions
|
||||
if !condition {
|
||||
t.Error("expected condition to be true")
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices for Assertions
|
||||
|
||||
- Provide descriptive assertion messages when failures need context
|
||||
- Prefer explicit assertion methods over manual comparisons
|
||||
- Use dedicated methods for error checking (`NoError`, `Error`)
|
||||
- Use `t.Fatalf()` for fatal errors that prevent further test execution
|
||||
- Use `t.Errorf()` for non-fatal errors to see all failures in a test
|
||||
|
||||
## Test Types and Organization
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Test individual functions and methods in isolation:
|
||||
|
||||
- **Use mocks/stubs** for external dependencies
|
||||
- **Focus on single units** of functionality
|
||||
- **Fast execution** (milliseconds)
|
||||
- **No external service dependencies**
|
||||
|
||||
**Example**: Testing a service function with mocked repository:
|
||||
|
||||
```go
|
||||
func TestUserService_CreateUser(t *testing.T) {
|
||||
mockRepo := &MockUserRepository{}
|
||||
service := NewUserService(mockRepo)
|
||||
|
||||
// Test the service logic in isolation
|
||||
user, err := service.CreateUser(context.Background(), userData)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, user)
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Test multiple components working together:
|
||||
|
||||
- **May use real database connections** or external services
|
||||
- **Test actual integration points** and workflows
|
||||
- **Slower execution** (seconds)
|
||||
- **Often use test containers** or local services
|
||||
|
||||
**Example**: Testing database integration:
|
||||
|
||||
```go
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
func TestUserRepository_Integration(t *testing.T) {
|
||||
db := setupTestDatabase(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := NewUserRepository(db)
|
||||
|
||||
// Test actual database operations
|
||||
user, err := repo.Create(context.Background(), userData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
retrieved, err := repo.GetByID(context.Background(), user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Email, retrieved.Email)
|
||||
}
|
||||
```
|
||||
|
||||
### Separating Test Types with Build Tags
|
||||
|
||||
**Consider using build tags** to separate test types:
|
||||
|
||||
```go
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package mypackage_test
|
||||
```
|
||||
|
||||
Run different test types separately:
|
||||
- Unit tests: `go test ./...`
|
||||
- Integration tests: `go test -tags=integration ./...`
|
||||
- All tests: `go test -tags=integration ./...`
|
||||
|
||||
## Mocking and Test Doubles
|
||||
|
||||
### Creating Mocks
|
||||
|
||||
For interfaces, create mock implementations:
|
||||
|
||||
```go
|
||||
type MockUserRepository struct {
|
||||
CreateFunc func(ctx context.Context, user *User) error
|
||||
GetFunc func(ctx context.Context, id string) (*User, error)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) Create(ctx context.Context, user *User) error {
|
||||
if m.CreateFunc != nil {
|
||||
return m.CreateFunc(ctx, user)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) Get(ctx context.Context, id string) (*User, error) {
|
||||
if m.GetFunc != nil {
|
||||
return m.GetFunc(ctx, id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Using Mocking Libraries
|
||||
|
||||
Consider using mocking libraries for complex interfaces:
|
||||
- `github.com/stretchr/testify/mock` - Popular mocking framework
|
||||
- `github.com/golang/mock/gomock` - Official Go mocking library
|
||||
|
||||
## Test Setup and Teardown
|
||||
|
||||
### Test Helpers
|
||||
|
||||
Create helper functions for common setup:
|
||||
|
||||
```go
|
||||
func setupTest(t *testing.T) (*Service, func()) {
|
||||
t.Helper()
|
||||
|
||||
// Setup
|
||||
service := NewService(/* dependencies */)
|
||||
|
||||
// Return cleanup function
|
||||
cleanup := func() {
|
||||
// Teardown code
|
||||
}
|
||||
|
||||
return service, cleanup
|
||||
}
|
||||
|
||||
func TestSomething(t *testing.T) {
|
||||
service, cleanup := setupTest(t)
|
||||
defer cleanup()
|
||||
|
||||
// Test code
|
||||
}
|
||||
```
|
||||
|
||||
### Table Test Setup
|
||||
|
||||
For table-driven tests, use setup functions when needed:
|
||||
|
||||
```go
|
||||
func TestWithSetup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) *Service
|
||||
// other fields
|
||||
}{
|
||||
{
|
||||
name: "test case 1",
|
||||
setup: func(t *testing.T) *Service {
|
||||
return NewService(/* specific config */)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
service := tt.setup(t)
|
||||
// test code
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
1. **Test behavior, not implementation**: Tests should verify outcomes, not internal mechanics
|
||||
2. **Keep tests independent**: Each test should run in isolation without depending on others
|
||||
3. **Use meaningful test data**: Test values should be realistic and representative
|
||||
4. **Test edge cases**: Include boundary values, empty inputs, nil values
|
||||
5. **Test error paths**: Verify error handling and error messages
|
||||
6. **Keep tests maintainable**: Refactor tests as you refactor code
|
||||
7. **Use t.Helper()**: Mark helper functions with `t.Helper()` for better error reporting
|
||||
8. **Run tests frequently**: Run tests during development, not just before commits
|
||||
9. **Keep tests fast**: Slow tests discourage frequent running
|
||||
10. **Document complex test scenarios**: Add comments explaining non-obvious test setups
|
||||
|
||||
## Common Testing Patterns
|
||||
|
||||
### Testing with Context
|
||||
|
||||
```go
|
||||
func TestWithContext(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result, err := service.DoSomething(ctx)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Concurrent Code
|
||||
|
||||
```go
|
||||
func TestConcurrency(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := service.DoWork(); err != nil {
|
||||
errors <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
for err := range errors {
|
||||
t.Errorf("concurrent operation failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Time-Dependent Code
|
||||
|
||||
Use time interfaces or dependency injection:
|
||||
|
||||
```go
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// In tests, use a fake clock
|
||||
type FakeClock struct {
|
||||
current time.Time
|
||||
}
|
||||
|
||||
func (f *FakeClock) Now() time.Time {
|
||||
return f.current
|
||||
}
|
||||
```
|
||||
|
||||
## Test Coverage Analysis
|
||||
|
||||
Run coverage analysis to identify untested code:
|
||||
|
||||
```bash
|
||||
# Generate coverage report
|
||||
go test -coverprofile=coverage.out ./...
|
||||
|
||||
# View coverage in browser
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Show coverage percentage
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
Focus coverage efforts on:
|
||||
- Critical business logic
|
||||
- Complex algorithms
|
||||
- Error handling paths
|
||||
- Edge cases and boundary conditions
|
||||
Reference in New Issue
Block a user