- Introduced a new skill for Golang, providing comprehensive guidance on writing, debugging, and best practices for Go programming. - Added reference materials including effective Go guidelines, common patterns, and a quick reference cheat sheet to support users in Go development. - Created a skill creator guide to assist in developing new skills with structured templates and resource management. - Implemented scripts for skill initialization and packaging to streamline the skill creation process.
650 lines
12 KiB
Markdown
650 lines
12 KiB
Markdown
# Go Common Patterns and Idioms
|
|
|
|
## Design Patterns
|
|
|
|
### Functional Options Pattern
|
|
|
|
Used for configuring objects with many optional parameters:
|
|
|
|
```go
|
|
type Server struct {
|
|
host string
|
|
port int
|
|
timeout time.Duration
|
|
maxConn int
|
|
}
|
|
|
|
type Option func(*Server)
|
|
|
|
func WithHost(host string) Option {
|
|
return func(s *Server) {
|
|
s.host = host
|
|
}
|
|
}
|
|
|
|
func WithPort(port int) Option {
|
|
return func(s *Server) {
|
|
s.port = port
|
|
}
|
|
}
|
|
|
|
func WithTimeout(timeout time.Duration) Option {
|
|
return func(s *Server) {
|
|
s.timeout = timeout
|
|
}
|
|
}
|
|
|
|
func NewServer(opts ...Option) *Server {
|
|
// Set defaults
|
|
s := &Server{
|
|
host: "localhost",
|
|
port: 8080,
|
|
timeout: 30 * time.Second,
|
|
maxConn: 100,
|
|
}
|
|
|
|
// Apply options
|
|
for _, opt := range opts {
|
|
opt(s)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Usage
|
|
srv := NewServer(
|
|
WithHost("example.com"),
|
|
WithPort(443),
|
|
WithTimeout(60 * time.Second),
|
|
)
|
|
```
|
|
|
|
### Builder Pattern
|
|
|
|
For complex object construction:
|
|
|
|
```go
|
|
type HTTPRequest struct {
|
|
method string
|
|
url string
|
|
headers map[string]string
|
|
body []byte
|
|
}
|
|
|
|
type RequestBuilder struct {
|
|
request *HTTPRequest
|
|
}
|
|
|
|
func NewRequestBuilder() *RequestBuilder {
|
|
return &RequestBuilder{
|
|
request: &HTTPRequest{
|
|
headers: make(map[string]string),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (b *RequestBuilder) Method(method string) *RequestBuilder {
|
|
b.request.method = method
|
|
return b
|
|
}
|
|
|
|
func (b *RequestBuilder) URL(url string) *RequestBuilder {
|
|
b.request.url = url
|
|
return b
|
|
}
|
|
|
|
func (b *RequestBuilder) Header(key, value string) *RequestBuilder {
|
|
b.request.headers[key] = value
|
|
return b
|
|
}
|
|
|
|
func (b *RequestBuilder) Body(body []byte) *RequestBuilder {
|
|
b.request.body = body
|
|
return b
|
|
}
|
|
|
|
func (b *RequestBuilder) Build() *HTTPRequest {
|
|
return b.request
|
|
}
|
|
|
|
// Usage
|
|
req := NewRequestBuilder().
|
|
Method("POST").
|
|
URL("https://api.example.com").
|
|
Header("Content-Type", "application/json").
|
|
Body([]byte(`{"key":"value"}`)).
|
|
Build()
|
|
```
|
|
|
|
### Singleton Pattern
|
|
|
|
Thread-safe singleton using sync.Once:
|
|
|
|
```go
|
|
type Database struct {
|
|
conn *sql.DB
|
|
}
|
|
|
|
var (
|
|
instance *Database
|
|
once sync.Once
|
|
)
|
|
|
|
func GetDatabase() *Database {
|
|
once.Do(func() {
|
|
conn, err := sql.Open("postgres", "connection-string")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
instance = &Database{conn: conn}
|
|
})
|
|
return instance
|
|
}
|
|
```
|
|
|
|
### Factory Pattern
|
|
|
|
```go
|
|
type Animal interface {
|
|
Speak() string
|
|
}
|
|
|
|
type Dog struct{}
|
|
func (d Dog) Speak() string { return "Woof!" }
|
|
|
|
type Cat struct{}
|
|
func (c Cat) Speak() string { return "Meow!" }
|
|
|
|
type AnimalFactory struct{}
|
|
|
|
func (f *AnimalFactory) CreateAnimal(animalType string) Animal {
|
|
switch animalType {
|
|
case "dog":
|
|
return &Dog{}
|
|
case "cat":
|
|
return &Cat{}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
```
|
|
|
|
### Strategy Pattern
|
|
|
|
```go
|
|
type PaymentStrategy interface {
|
|
Pay(amount float64) error
|
|
}
|
|
|
|
type CreditCard struct {
|
|
number string
|
|
}
|
|
|
|
func (c *CreditCard) Pay(amount float64) error {
|
|
fmt.Printf("Paying %.2f using credit card %s\n", amount, c.number)
|
|
return nil
|
|
}
|
|
|
|
type PayPal struct {
|
|
email string
|
|
}
|
|
|
|
func (p *PayPal) Pay(amount float64) error {
|
|
fmt.Printf("Paying %.2f using PayPal account %s\n", amount, p.email)
|
|
return nil
|
|
}
|
|
|
|
type PaymentContext struct {
|
|
strategy PaymentStrategy
|
|
}
|
|
|
|
func (pc *PaymentContext) SetStrategy(strategy PaymentStrategy) {
|
|
pc.strategy = strategy
|
|
}
|
|
|
|
func (pc *PaymentContext) ExecutePayment(amount float64) error {
|
|
return pc.strategy.Pay(amount)
|
|
}
|
|
```
|
|
|
|
## Concurrency Patterns
|
|
|
|
### Worker Pool
|
|
|
|
```go
|
|
func worker(id int, jobs <-chan Job, results chan<- Result) {
|
|
for job := range jobs {
|
|
result := processJob(job)
|
|
results <- result
|
|
}
|
|
}
|
|
|
|
func WorkerPool(numWorkers int, jobs []Job) []Result {
|
|
jobsChan := make(chan Job, len(jobs))
|
|
results := make(chan Result, len(jobs))
|
|
|
|
// Start workers
|
|
for w := 1; w <= numWorkers; w++ {
|
|
go worker(w, jobsChan, results)
|
|
}
|
|
|
|
// Send jobs
|
|
for _, job := range jobs {
|
|
jobsChan <- job
|
|
}
|
|
close(jobsChan)
|
|
|
|
// Collect results
|
|
var output []Result
|
|
for range jobs {
|
|
output = append(output, <-results)
|
|
}
|
|
|
|
return output
|
|
}
|
|
```
|
|
|
|
### Pipeline Pattern
|
|
|
|
```go
|
|
func generator(nums ...int) <-chan int {
|
|
out := make(chan int)
|
|
go func() {
|
|
for _, n := range nums {
|
|
out <- n
|
|
}
|
|
close(out)
|
|
}()
|
|
return out
|
|
}
|
|
|
|
func square(in <-chan int) <-chan int {
|
|
out := make(chan int)
|
|
go func() {
|
|
for n := range in {
|
|
out <- n * n
|
|
}
|
|
close(out)
|
|
}()
|
|
return out
|
|
}
|
|
|
|
func main() {
|
|
// Create pipeline
|
|
c := generator(2, 3, 4)
|
|
out := square(c)
|
|
|
|
// Consume output
|
|
for result := range out {
|
|
fmt.Println(result)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Fan-Out, Fan-In
|
|
|
|
```go
|
|
func fanOut(in <-chan int, n int) []<-chan int {
|
|
channels := make([]<-chan int, n)
|
|
for i := 0; i < n; i++ {
|
|
channels[i] = worker(in)
|
|
}
|
|
return channels
|
|
}
|
|
|
|
func worker(in <-chan int) <-chan int {
|
|
out := make(chan int)
|
|
go func() {
|
|
for n := range in {
|
|
out <- expensiveOperation(n)
|
|
}
|
|
close(out)
|
|
}()
|
|
return out
|
|
}
|
|
|
|
func fanIn(channels ...<-chan int) <-chan int {
|
|
out := make(chan int)
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Add(len(channels))
|
|
for _, c := range channels {
|
|
go func(ch <-chan int) {
|
|
defer wg.Done()
|
|
for n := range ch {
|
|
out <- n
|
|
}
|
|
}(c)
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(out)
|
|
}()
|
|
|
|
return out
|
|
}
|
|
```
|
|
|
|
### Timeout Pattern
|
|
|
|
```go
|
|
func DoWithTimeout(timeout time.Duration) (result string, err error) {
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
result = expensiveOperation()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
return result, nil
|
|
case <-time.After(timeout):
|
|
return "", fmt.Errorf("operation timed out after %v", timeout)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Graceful Shutdown
|
|
|
|
```go
|
|
func main() {
|
|
server := &http.Server{Addr: ":8080"}
|
|
|
|
// Start server in goroutine
|
|
go func() {
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("listen: %s\n", err)
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt signal
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
log.Println("Shutting down server...")
|
|
|
|
// Graceful shutdown with timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := server.Shutdown(ctx); err != nil {
|
|
log.Fatal("Server forced to shutdown:", err)
|
|
}
|
|
|
|
log.Println("Server exiting")
|
|
}
|
|
```
|
|
|
|
### Rate Limiting
|
|
|
|
```go
|
|
func rateLimiter(rate time.Duration) <-chan time.Time {
|
|
return time.Tick(rate)
|
|
}
|
|
|
|
func main() {
|
|
limiter := rateLimiter(200 * time.Millisecond)
|
|
|
|
for req := range requests {
|
|
<-limiter // Wait for rate limiter
|
|
go handleRequest(req)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Circuit Breaker
|
|
|
|
```go
|
|
type CircuitBreaker struct {
|
|
maxFailures int
|
|
timeout time.Duration
|
|
failures int
|
|
lastFail time.Time
|
|
state string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (cb *CircuitBreaker) Call(fn func() error) error {
|
|
cb.mu.Lock()
|
|
defer cb.mu.Unlock()
|
|
|
|
if cb.state == "open" {
|
|
if time.Since(cb.lastFail) > cb.timeout {
|
|
cb.state = "half-open"
|
|
} else {
|
|
return fmt.Errorf("circuit breaker is open")
|
|
}
|
|
}
|
|
|
|
err := fn()
|
|
if err != nil {
|
|
cb.failures++
|
|
cb.lastFail = time.Now()
|
|
if cb.failures >= cb.maxFailures {
|
|
cb.state = "open"
|
|
}
|
|
return err
|
|
}
|
|
|
|
cb.failures = 0
|
|
cb.state = "closed"
|
|
return nil
|
|
}
|
|
```
|
|
|
|
## Error Handling Patterns
|
|
|
|
### Error Wrapping
|
|
|
|
```go
|
|
func processFile(filename string) (err error) {
|
|
data, err := readFile(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to process file %s: %w", filename, err)
|
|
}
|
|
|
|
if err := validate(data); err != nil {
|
|
return fmt.Errorf("validation failed for %s: %w", filename, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### Sentinel Errors
|
|
|
|
```go
|
|
var (
|
|
ErrNotFound = errors.New("not found")
|
|
ErrUnauthorized = errors.New("unauthorized")
|
|
ErrInvalidInput = errors.New("invalid input")
|
|
)
|
|
|
|
func FindUser(id int) (*User, error) {
|
|
user, exists := users[id]
|
|
if !exists {
|
|
return nil, ErrNotFound
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// Check error
|
|
user, err := FindUser(123)
|
|
if errors.Is(err, ErrNotFound) {
|
|
// Handle not found
|
|
}
|
|
```
|
|
|
|
### Custom Error Types
|
|
|
|
```go
|
|
type ValidationError struct {
|
|
Field string
|
|
Value interface{}
|
|
Err error
|
|
}
|
|
|
|
func (e *ValidationError) Error() string {
|
|
return fmt.Sprintf("validation failed for field %s with value %v: %v",
|
|
e.Field, e.Value, e.Err)
|
|
}
|
|
|
|
func (e *ValidationError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// Usage
|
|
var validErr *ValidationError
|
|
if errors.As(err, &validErr) {
|
|
fmt.Printf("Field: %s\n", validErr.Field)
|
|
}
|
|
```
|
|
|
|
## Resource Management Patterns
|
|
|
|
### Defer for Cleanup
|
|
|
|
```go
|
|
func processFile(filename string) error {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
// Process file
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### Context for Cancellation
|
|
|
|
```go
|
|
func fetchData(ctx context.Context, url string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
```
|
|
|
|
### Sync.Pool for Object Reuse
|
|
|
|
```go
|
|
var bufferPool = sync.Pool{
|
|
New: func() interface{} {
|
|
return new(bytes.Buffer)
|
|
},
|
|
}
|
|
|
|
func process() {
|
|
buf := bufferPool.Get().(*bytes.Buffer)
|
|
defer bufferPool.Put(buf)
|
|
|
|
buf.Reset()
|
|
// Use buffer
|
|
}
|
|
```
|
|
|
|
## Testing Patterns
|
|
|
|
### Table-Driven Tests
|
|
|
|
```go
|
|
func TestAdd(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
a, b int
|
|
expected int
|
|
}{
|
|
{"positive numbers", 2, 3, 5},
|
|
{"negative numbers", -1, -1, -2},
|
|
{"mixed signs", -5, 10, 5},
|
|
{"zeros", 0, 0, 0},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := Add(tt.a, tt.b)
|
|
if result != tt.expected {
|
|
t.Errorf("Add(%d, %d) = %d; want %d",
|
|
tt.a, tt.b, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Mock Interfaces
|
|
|
|
```go
|
|
type Database interface {
|
|
Get(key string) (string, error)
|
|
Set(key, value string) error
|
|
}
|
|
|
|
type MockDB struct {
|
|
data map[string]string
|
|
}
|
|
|
|
func (m *MockDB) Get(key string) (string, error) {
|
|
val, ok := m.data[key]
|
|
if !ok {
|
|
return "", errors.New("not found")
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
func (m *MockDB) Set(key, value string) error {
|
|
m.data[key] = value
|
|
return nil
|
|
}
|
|
|
|
func TestUserService(t *testing.T) {
|
|
mockDB := &MockDB{data: make(map[string]string)}
|
|
service := NewUserService(mockDB)
|
|
// Test service
|
|
}
|
|
```
|
|
|
|
### Test Fixtures
|
|
|
|
```go
|
|
func setupTestDB(t *testing.T) (*sql.DB, func()) {
|
|
db, err := sql.Open("sqlite3", ":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Setup schema
|
|
_, err = db.Exec(schema)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup := func() {
|
|
db.Close()
|
|
}
|
|
|
|
return db, cleanup
|
|
}
|
|
|
|
func TestDatabase(t *testing.T) {
|
|
db, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Run tests
|
|
}
|
|
```
|
|
|