Type Embedding: Building Composable Structs

Go allows one struct to embed another, enabling shared behavior and data without the complexity of traditional object-oriented inheritance. This is particularly useful when modeling relationships between entities that share common behavior.

Consider a simple logging system where multiple components need to support logging:

type Logger struct {
    Name string
}

func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.Name, msg)
}

type HTTPClient struct {
    Logger
    BaseURL string
}

func (c *HTTPClient) Fetch() {
    c.Log("Fetching data from " + c.BaseURL)
}

In this example, HTTPClient embeds Logger, gaining access to its Log method. When Fetch is called, it delegates the logging call to the embedded Logger. This avoids the need to explicitly define Log on HTTPClient, promoting reuse and clarity.

Best Practice: Embed types that provide orthogonal behavior to the embedding struct. Avoid embedding types that are too tightly coupled or contain internal state not relevant to the outer struct.


Interface Composition: Designing for Flexibility

Interface composition is a key Go idiom that allows developers to build interfaces by combining smaller, more focused interfaces. This enables greater flexibility in designing components that can be composed and reused in various contexts.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

In this example, ReadCloser is composed from Reader and Closer. Any type that implements both interfaces satisfies ReadCloser, which is a common pattern in the Go standard library. This promotes modular design and makes it easier to create new interfaces by combining existing ones.

Best Practice: Prefer small, single-responsibility interfaces over large, monolithic ones. This makes them easier to implement and compose.


Embedding Anonymous Interfaces in Structs

Anonymous interfaces can be embedded directly into struct definitions to enforce that a struct implements those interfaces. This is a powerful technique for ensuring that a type adheres to a specific set of behaviors at compile time.

type HTTPService struct {
    string
    io.ReadCloser
}

func (s *HTTPService) Read(p []byte) (n int, err error) {
    return s.ReadCloser.Read(p)
}

func (s *HTTPService) Close() error {
    return s.ReadCloser.Close()
}

In this example, HTTPService embeds io.ReadCloser, and by implementing Read and Close, it satisfies the embedded interface. This pattern is useful in middleware and adapter patterns where a struct must fulfill a specific interface contract.

Best Practice: Use embedded interfaces to enforce interface implementation and reduce boilerplate. However, avoid overusing this pattern for complex interface hierarchies.


Type Embedding vs. Interface Composition: When to Use Each

ConceptUse CaseData SharingMethod SharingBehavior Enforcement
Type EmbeddingReusing fields and methodsYesYesNo
Interface CompositionComposing behavior contractsNoNoYes

Type embedding is ideal for sharing both data and behavior, while interface composition is best for defining and composing behavior contracts. Understanding the distinction helps avoid design pitfalls and ensures idiomatic Go code.


Advanced Example: Combining Embedding and Composition

Let’s build a more complex example that combines both techniques. We’ll define a logging HTTP client that supports multiple output destinations using interface composition and type embedding.

type Logger interface {
    Log(msg string)
}

type ConsoleLogger struct{}

func (l *ConsoleLogger) Log(msg string) {
    fmt.Println("[Console] " + msg)
}

type FileLogger struct{}

func (l *FileLogger) Log(msg string) {
    fmt.Println("[File] " + msg) // In a real app, would write to a file
}

type MultiLogger struct {
    Loggers []Logger
}

func (m *MultiLogger) Log(msg string) {
    for _, l := range m.Loggers {
        l.Log(msg)
    }
}

type HTTPClient struct {
    Logger
    BaseURL string
}

func (c *HTTPClient) Fetch() {
    c.Log("Fetching data from " + c.BaseURL)
}

In this example, MultiLogger implements the Logger interface and can be embedded into HTTPClient. This allows HTTPClient to use any logger, making it highly flexible and testable.

Best Practice: Use embedding for shared data and behavior, and composition for defining behavior interfaces. This combination leads to highly modular and reusable Go code.


Learn more with useful resources