Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Refactoring Code: Choosing Functions Over Object-Oriented Structures in Go

Tech May 12 2

Recently I needed to write a new business module that closely resembled one I had written two years ago in Go. I initially planned to adopt the old code, but it turned out to be too tightly coupled to its original context. Small modificasions wouldn’t cut it, so I decided to refactor the core logic. The main change was moving from object-oriented abstractions toward simple, stateless functions.

Prefer functions that don’t carry unnecessary state

The original code forced many operations into objects, maintaining extra fields that weren’t truly needed. Consider a Go function that starts a Docker container using the SDK. A typical call looks like this:

err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})

Creating the cli can fail:

cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())

Getting the container ID also returns an error:

resp, err := cli.ContainerInspect(context.Background(), name)

So a single start operation involves three error sources. An object-oriented wrapper might look like this:

type ContainerManager struct {
    client        *client.Client
    ctx           context.Context
    containerName string
    containerID   string
}

func NewContainerManager(name string) (*ContainerManager, error) {
    cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
    if err != nil {
        return nil, err
    }
    ctx := context.Background()
    resp, err := cli.ContainerInspect(ctx, name)
    if err != nil {
        return nil, err
    }
    return &ContainerManager{
        client:        cli,
        ctx:           ctx,
        containerName: name,
        containerID:   resp.ID,
    }, nil
}

func (m *ContainerManager) Start() error {
    return m.client.ContainerStart(m.ctx, m.containerID, types.ContainerStartOptions{})
}

On the surface this seems convenient: the Start method reuses the cli and containerID stored in the struct. However, the constructor can fail, and any code that creates a ContainerManager must handle that error. Worse, if a ContainerManager is passed around and later used without checking for a nil pointer, calling Start() causes a panic. The error propagates outward, infecting every caller.

A cleaner approach is to avoid holding the mutable state altogether. Store only the container name, and let each method rebuild the required clients:

type ContainerManager struct {
    containerName string
}

func NewContainerManager(name string) *ContainerManager {
    return &ContainerManager{containerName: name}
}

func (m *ContainerManager) Start() error {
    cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
    if err != nil {
        return err
    }
    ctx := context.Background()
    resp, err := cli.ContainerInspect(ctx, m.containerName)
    if err != nil {
        return err
    }
    return cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
}

Now the consrtuctor never fails, and external code doesn’t need to worry about invalid objects. But this version is essentially a thin wrapper around a function – the object adds little value. Removing the object entirely makes the code even simpler:

func StartContainer(name string) error {
    cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
    if err != nil {
        return err
    }
    ctx := context.Background()
    resp, err := cli.ContainerInspect(ctx, name)
    if err != nil {
        return err
    }
    return cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
}

This function receives the container name as a parameter, creates and tears down its own dependencies, and returns a single unified error. There is no state to manage, no constructor to fail, and no risk of a nil-struct panic. For many scenarios – especially when the operation is self‑contained – this pattern is preferable to an object‑oriented wrapper.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.