L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

advanced-patterns

advanced-patterns

Applies advanced Go patterns including generics, reflection, functional options, and metaprogramming for performance-critical and framework-level code.

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: advanced-patterns description: Applies advanced Go patterns including generics, reflection, functional options, and metaprogramming for performance-critical and framework-level code. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: go role: implementation scope: implementation output-format: code content-types:

  • code
  • guidance
  • do-dont
  • examples triggers: go generics, go reflection, go unsafe, go functional options, go option pattern, go compile time, go metaprogramming archetypes:
  • tactical anti_triggers:
  • brainstorming
  • vague ideation response_profile: verbosity: low directive_strength: high abstraction_level: operational related-skills: best-practices, modular-design, concurrency-patterns, database-patterns maturity: stable completeness: 95 exampleCount: 3

Advanced Go Patterns

Senior Go engineer applying advanced language features for framework-level and performance-critical code. This skill covers generics, reflection, functional options, compile-time assertions, and safe metaprogramming.

TL;DR Checklist

  • Use generics for type-safe collections and algorithms — prefer over interfaces when possible
  • Use the functional options pattern for constructors with many optional parameters
  • Use go:embed for compile-time asset inclusion — never hardcode file paths
  • Use type _ interface{} compile-time assertions to verify interface satisfaction
  • Avoid reflection unless necessary — it bypasses the type system and is slow
  • Never use unsafe unless you fully understand the memory layout and aliasing rules

When to Use

Use this skill when:

  • Building a framework or library that needs type-safe generic APIs
  • Designing constructors with many optional parameters (functional options)
  • Need compile-time asset embedding (go:embed)
  • Writing performance-critical code where reflection overhead is unacceptable
  • Creating compile-time assertions to catch interface violations at build time

When NOT to Use

Avoid this skill for:

  • Application business logic — simple code is better than clever code
  • When a plain interface or struct method would solve the problem
  • Any code where readability matters more than abstraction
  • Code that will be maintained by developers unfamiliar with advanced Go patterns

Core Workflow

  1. Choose the Right Abstraction — Decide between interfaces, generics, or composition. Checkpoint: The abstraction adds value — it reduces code duplication or enables type safety.

  2. Implement the Pattern — Write the generic function, option type, or reflection logic. Checkpoint: Code compiles with go vet — no type safety violations.

  3. Add Compile-Time Assertions — Verify that types satisfy expected interfaces. Checkpoint: Compilation fails if the interface contract is violated.

  4. Benchmark Performance — Ensure the pattern doesn't add unacceptable overhead. Checkpoint: Benchmark shows acceptable performance vs. the non-abstracted version.

  5. Document the Pattern — Explain why the pattern was chosen and how to use it. Checkpoint: Usage examples are included in godoc comments.


Implementation Patterns

Pattern 1: Generics (❌ BAD vs ✅ GOOD)

Generics enable type-safe code without runtime type assertions.

❌ BAD — Interface-Based Collection

// ❌ BAD: interface{} requires runtime type assertions — unsafe and slow
type Stack struct {
	items []interface{}
}

func (s *Stack) Push(item interface{}) {
	s.items = append(s.items, item)
}

func (s *Stack) Pop() interface{} {
	if len(s.items) == 0 {
		return nil
	}
	item := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return item
}

// Usage requires type assertion — easy to get wrong
func processStack() {
	s := &Stack{}
	s.Push("hello")
	s.Push(42) // compiles — but wrong type!

	// Runtime type assertion — panics if type is wrong
	str := s.Pop().(string) // panics if the top item was 42
	fmt.Println(str)
}

What's wrong:

  • interface{} accepts any type — s.Push(42) compiles but is semantically wrong
  • Pop().(string) panics at runtime if the type is wrong
  • No compile-time safety — type errors are runtime errors
  • interface{} has allocation overhead — each value is boxed
  • No documentation of the expected element type

✅ GOOD — Generic Stack

// Stack is a generic LIFO (last-in, first-out) data structure.
// The element type T is specified at construction time.
type Stack[T any] struct {
	items []T
}

// NewStack creates a new generic stack.
func NewStack[T any]() *Stack[T] {
	return &Stack[T]{items: make([]T, 0)}
}

// Push adds an item to the top of the stack.
func (s *Stack[T]) Push(item T) {
	s.items = append(s.items, item)
}

// Pop removes and returns the top item.
// Returns zero value of T and false if the stack is empty.
func (s *Stack[T]) Pop() (T, bool) {
	if len(s.items) == 0 {
		var zero T
		return zero, false
	}
	item := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return item, true
}

// Len returns the number of items in the stack.
func (s *Stack[T]) Len() int {
	return len(s.items)
}

// IsEmpty returns true if the stack has no items.
func (s *Stack[T]) IsEmpty() bool {
	return len(s.items) == 0
}

// Usage — type is enforced at compile time
func processStack() {
	// String stack
	strStack := NewStack[string]()
	strStack.Push("hello")
	strStack.Push("world")

	str, ok := strStack.Pop()
	if !ok {
		log.Fatal("stack is empty")
	}
	fmt.Println(str) // "world"

	// This would not compile — type is enforced:
	// strStack.Push(42) // ❌ cannot use 42 (int) as string

	// Int stack — separate type
	intStack := NewStack[int]()
	intStack.Push(42)
	intStack.Push(100)

	n, ok := intStack.Pop()
	if ok {
		fmt.Println(n) // 100
	}
}

Why this works:

  • Type is specified at construction time — NewStack[string]() creates a string stack
  • Compile-time enforcement — strStack.Push(42) does not compile
  • No runtime type assertions — Pop() returns (T, bool) — zero value on empty
  • No allocation overhead — values are stored directly, not boxed as interface{}
  • Clear API — IsEmpty(), Len(), and the (T, bool) return make usage obvious

Pattern 2: Functional Options (❌ BAD vs ✅ GOOD)

The functional options pattern provides a clean API for constructors with many optional parameters.

❌ BAD — Many Optional Parameters

// ❌ BAD: too many parameters — hard to use, easy to forget required ones
type Server struct {
	Addr      string
	Port      int
	Timeout   time.Duration
	CertFile  string
	KeyFile   string
	MaxConns  int
	KeepAlive bool
	ReadBuf   int
	WriteBuf  int
	Debug     bool
}

// Constructor with 10 parameters — 7 optional, 3 required
func NewServer(addr string, port int, timeout time.Duration,
	certFile string, keyFile string, maxConns int,
	keepAlive bool, readBuf int, writeBuf int, debug bool) *Server {
	// ...
}

// Usage — which parameters are optional? What are the defaults?
// Hard to read, easy to mess up
server := NewServer("0.0.0.0", 8080, 30*time.Second,
	"cert.pem", "key.pem", 100,
	true, 4096, 4096, false)

What's wrong:

  • 10 parameters — impossible to remember which are required vs. optional
  • No documentation of defaults — caller must guess
  • Order matters — swapping certFile and keyFile is a silent bug
  • Adding a new parameter breaks all callers
  • Hard to set just one option without specifying all others

✅ GOOD — Functional Options Pattern

// Server provides an HTTP server with configurable options.
type Server struct {
	addr       string
	port       int
	timeout    time.Duration
	certFile   string
	keyFile    string
	maxConns   int
	keepAlive  bool
	readBuf    int
	writeBuf   int
	debug      bool
}

// Option configures a Server via functional options.
type Option func(*Server)

// WithAddr sets the server address. Default: "0.0.0.0".
func WithAddr(addr string) Option {
	return func(s *Server) {
		s.addr = addr
	}
}

// WithPort sets the server port. Default: 8080.
func WithPort(port int) Option {
	return func(s *Server) {
		s.port = port
	}
}

// WithTimeout sets the read/write timeout. Default: 30s.
func WithTimeout(timeout time.Duration) Option {
	return func(s *Server) {
		s.timeout = timeout
	}
}

// WithTLS sets TLS certificate and key files.
func WithTLS(certFile, keyFile string) Option {
	return func(s *Server) {
		s.certFile = certFile
		s.keyFile = keyFile
	}
}

// WithMaxConns sets the maximum number of connections. Default: 100.
func WithMaxConns(maxConns int) Option {
	return func(s *Server) {
		s.maxConns = maxConns
	}
}

// WithDebug enables debug logging. Default: false.
func WithDebug(enabled bool) Option {
	return func(s *Server) {
		s.debug = enabled
	}
}

// NewServer creates a Server with the given options.
// All options are optional — defaults are applied for unset values.
func NewServer(options ...Option) *Server {
	s := &Server{
		addr:     "0.0.0.0",
		port:     8080,
		timeout:  30 * time.Second,
		maxConns: 100,
		readBuf:  4096,
		writeBuf: 4096,
	}

	// Apply options in order — last one wins for conflicting options
	for _, opt := range options {
		opt(s)
	}

	return s
}

// Usage — clean, readable, only specify what you need
server := NewServer(
	WithPort(9090),
	WithTimeout(60*time.Second),
	WithTLS("cert.pem", "key.pem"),
	WithDebug(true),
)

// Minimal usage — all defaults
server := NewServer()

// Override a single default
server := NewServer(WithPort(443))

Why this works:

  • Each option is a self-documenting function — WithPort(9090) is readable
  • Order doesn't matter — options are applied sequentially in NewServer
  • Adding options is easy — new WithXxx functions don't break existing callers
  • Defaults are explicit in NewServer — caller knows what happens with no options
  • Type-safe — WithPort("abc") doesn't compile

Pattern 3: Compile-Time Assertions and go:embed (❌ BAD vs ✅ GOOD)

Compile-time assertions catch interface violations at build time. go:embed includes assets at compile time.

❌ BAD — Runtime Interface Checking

// ❌ BAD: interface satisfaction checked at runtime — too late
type Repository interface {
	Get(ctx context.Context, id string) (*User, error)
	Create(ctx context.Context, user *User) error
}

type MockRepository struct{}

func (m *MockRepository) Get(ctx context.Context, id string) (*User, error) {
	return nil, nil
}

// No compile-time check — if MockRepository doesn't implement Repository,
// the error surfaces only when the code is compiled (and only if someone
// tries to assign it)
func main() {
	// This would silently fail to compile if MockRepository is missing a method,
	// but only at the assignment site — no clear error message
	var repo Repository = &MockRepository{}
	_ = repo
}

// ❌ BAD: hardcoding asset paths — breaks if files move
func loadTemplate() string {
	data, err := os.ReadFile("templates/profile.html")
	if err != nil {
		log.Fatal(err)
	}
	return string(data)
}

What's wrong:

  • No compile-time guarantee that MockRepository implements Repository
  • If a method is added to the interface, the mock won't compile — but only at the usage site
  • Hardcoded file paths — fragile, breaks on different machines/environments
  • os.ReadFile at runtime — template loading happens on every call

✅ GOOD — Compile-Time Assertions with go:embed

// package: internal/user

// Compile-time interface assertion.
// This line will fail to compile if *MockRepository does NOT implement UserRepository.
var _ UserRepository = (*MockRepository)(nil)

// MockRepository implements UserRepository for testing.
type MockRepository struct {
	users map[string]*User
}

func NewMockRepository() *MockRepository {
	return &MockRepository{users: make(map[string]*User)}
}

func (m *MockRepository) Get(ctx context.Context, id string) (*User, error) {
	user, ok := m.users[id]
	if !ok {
		return nil, fmt.Errorf("user %s not found", id)
	}
	return user, nil
}

func (m *MockRepository) Create(ctx context.Context, user *User) error {
	if _, ok := m.users[user.ID]; ok {
		return fmt.Errorf("user %s already exists", user.ID)
	}
	m.users[user.ID] = user
	return nil
}

// Multiple interface assertions in one line
var (
	_ UserRepository = (*MockRepository)(nil)
	_ io.Reader      = (*MockRepository)(nil)
	_ io.Closer      = (*MockRepository)(nil)
)
// package: web

//go:embed templates/*.html
var templateFS embed.FS

// templateCache holds pre-parsed templates.
var templateCache = sync.OnceValue(func() *template.Template {
	tmpl := template.Must(template.New("").ParseFS(templateFS, "templates/*.html"))
	return tmpl
})

// renderProfile renders the user profile page.
func renderProfile(w http.ResponseWriter, user *User) {
	tmpl := templateCache()
	if err := tmpl.ExecuteTemplate(w, "profile.html", user); err != nil {
		log.Printf("template error: %v", err)
		http.Error(w, "internal error", 500)
	}
}

Directory structure:

myapp/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── user/
│   │   └── repository.go      // UserRepository interface + MockRepository
│   └── web/
│       ├── handler.go         // renderProfile with go:embed
│       └── templates/
│           ├── profile.html
│           ├── list.html
│           └── error.html
├── go.mod
└── go.sum

Why this works:

  • var _ UserRepository = (*MockRepository)(nil) — fails to compile if the mock doesn't implement the interface
  • The nil pointer (*MockRepository)(nil) is never created at runtime — it's a compile-time only check
  • //go:embed includes HTML templates in the binary at compile time — no runtime file reads
  • sync.OnceValue (Go 1.23+) lazily parses templates once — zero overhead on first access, cached thereafter
  • Template paths are relative to the source file — no hardcoded strings
  • Templates are bundled in the binary — deployment is a single binary

Constraints

MUST DO

  • MUST use generics for type-safe collections and algorithms — prefer over interface{} when the type is known at construction
  • MUST use the functional options pattern for constructors with more than 3 optional parameters
  • MUST use //go:embed for including static assets — never hardcode file paths
  • MUST add compile-time interface assertions with var _ Interface = (*Type)(nil)
  • MUST document generic type parameters with godoc comments (// T is the element type)
  • MUST benchmark any code that uses reflection — ensure overhead is acceptable

MUST NOT DO

  • MUST NOT use reflection to access private fields — it bypasses the type system and is fragile
  • MUST NOT use unsafe without a thorough understanding of memory layout and aliasing rules
  • MUST NOT overuse generics — simple interfaces or concrete types are often clearer
  • MUST NOT use functional options for constructors with fewer than 3 optional parameters — plain structs are simpler
  • MUST NOT embed files with go:embed that change frequently — embedded files are baked into the binary at compile time
  • MUST NOT use type _ interface{} assertions in production code without a clear reason — they add maintenance overhead

Related Skills

SkillPurpose
best-practicesGo idioms and code organization
modular-designInterface-based design that works with generics
concurrency-patternsGeneric concurrency utilities (channels, workers)
database-patternsGeneric repository patterns for type-safe data access
Skills enregistrées