Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:22:50 +08:00
commit e6ac7edfc0
14 changed files with 2164 additions and 0 deletions

View 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

View File

@@ -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