← Back to list

Rethinking Idioms: Go Constructor

The constructor function is a well established practice in the golang community, however a simple modification to this idiomatic pattern…

Chris Halbert · 2026-01-23 17:04 · 1 claps · 7.5 min read
#golang #design-pattern-in-golang #software-engineering #unit-testing #constructor-function
Open on Medium ↗
Wiki topics: 🎮 · Gaming

Rethinking Idioms: Go Constructor

The constructor function is a well established practice in the golang community, however a simple modification to this idiomatic pattern can make your code more testable for you, and more importantly, those using your library.

You may be asking yourself, why is it more important for your library to be easier to test by others?

One of Uncle Bob’s many reasons for writing Clean Code is that “the ratio of time spent reading vs. writing is well over 10:1.” To me, there are two major takeaways from this quote:

  1. Engineering Empathy — write your code for the consumption of others, because second to the CPU, they’re your biggest consumer.
  2. Scale — spending the extra time to writing clean code once, returns the investment through a reduction in inquiries n times.

Similarly, when we write our code using a more testable approach, we make the lives of our library consumers easier; they don’t have to do backflips to test their own code due to your code’s shortcomings.

With regards to scale, (admittedly a perspective for the greater good) you simplify the net sum of effort needed to write testable code for those using your library universally.

Idiomatic Constructor

Below introduces a basic example of the defacto constructor function implemented by a fictious team named the M&A Team, a clever abbreviation for Medium Article.

// medium_article.go

// MediumArticle is a concrete implementation representing an article
type MediumArticle struct {
    id    string
    title string
}

// loadArticle gets MediumArticle data from the API.
func loadArticle(ctx context.Context, id string) (MediumArticle, error) {
    // Assume api is an external package that we can't mock
    return api.GetMediumArticle(ctx, id)
}

// NewMediumArticle returns a fully initialized MediumArticle.
func NewMediumArticle(ctx context.Context, id string) (*MediumArticle, error) {
    article, err := loadArticle(ctx, id)
    if err != nil {
        return nil, err
    }

    return &MediumArticle{
        id:    id,
        title: article.Title,
    }, nil
}

// Title is an accessor method
func (m *MediumArticle) Title() string {
    return m.title
}

Let’s decompose their approach.

  • MediumArticle struct — The definition for the concrete MediumArticle type. The id is the hash for a medium article and the title is the name of the article.
  • func loadArticle— The M&A team is using this helper function to hit the Medium api and return a MediumArticle. The implementation is not real. For sake of example, this method illustrates an external dependency, which makes testing more complex.
  • func NewMediumArticle — This is the M&A Team’s implementation of an idiomatic golang constructor function returning a pointer receiver to a concrete implementation. While there are situations that call for returning an interface from a constructor, it’s generally the exception to the rule.
  • func Title — The M&A Team also included an accessor function to get the title.

Usage

Let’s consider a simple command line script implemented by the Slugs, another fictious group. Their core code slugifies a MediumArticle’s title to generate an SEO friendly URL.

// slugify.go
type Titled interface {
    Title() string
}

func SlugifyTitle(article Titled) string {
    regex := regexp.MustCompile("[^a-zA-Z0-9]+")
    return strings.ToLower(
        regex.ReplaceAllString(article.Title(), "-"),
    )
}

// cmd.go
func Run(ctx context.Context) (string, error) {
    article, err := NewMediumArticle(ctx, os.Args[1])
    if err != nil {
        return "", err
    }

    return SlugifyTitle(article), nil
}

// slugify_test.go
type MockArticle struct{}

func (m *MockArticle) Title() string {
    return "Rethinking Idioms: Go Constructor"
}

func TestSlugifyTitle(t *testing.T) {
    dummy := &MockArticle{}

    expected := "rethinking-idioms--go-constructor"
    actual := SlugifyTitle(dummy)

    assert.Equal(t, expected, actual)
}
  • Titled interface — Since the Slugs only need to use the Title() method, they define an interface that their SlugifyTitle function will use.
  • func SlugifyTitle — This is the Slugs’ implementation to slugify a title. It replaces all non-alphanumeric characters of a Titled interface‘s title to dashes and converts it to lower case.
  • func Run()—This is the entry point for the command line script. The cli user passes in the hash id of a MediumArticle and the Run() function loads a new MediumArticle and uses that to SlugifyTitle().
  • slugify_test.go — The Slugs define another type of MockArticle by creating a struct that implements the Titled interface: MockArticle. The Slugs use this struct to test their slugify.go code using Dependency Injection, without adding any additional overhead to their code in slugify.go.

Ok, if everything looks good and works, why are we trying to change a good thing?

Idiomatic Impediments

  • Can’t test all the things — While the current implementation of medium_article.go successfully sets up the Slugs team for test driven success, the Slugs team still are not testing the cmd.go script in the most simplistic state because they are using the M&A Team’s unmockable NewMediumArticle().
  • Focus on the SUT — Since it cannot be mocked, any test the Slugs writes to test cmd.go will implicitly test the M&A Team's medium_article.go code. The Slugs only should have to worry about verifying functionality of their SUT (system under test), so they implement a new approach so they can essentially wrap the M&A Team’s functionality:
// cmd.go
type ArticleLoader func(ctx context.Context, id string) (*MediumArticle, error)

func RunWithLoader(
    ctx context.Context,
    id string,
    load ArticleLoader,
) (string, error) {
    article, err := load(ctx, id)
    if err != nil {
        return "", err
    }

    return SlugifyTitle(article), nil
}

func Run(ctx context.Context) (string, error) {
    return RunWithLoader(ctx, os.Args[1], NewMediumArticle)
}

// cmd_test.go
func TestRunWithLoader(t *testing.T) {
    fakeLoader := func(ctx context.Context, id string) (*MediumArticle, error) {
        return &MediumArticle{
            id:    id,
            title: "Rethinking Idioms: Go Constructor",
        }, nil
    }

    result, err := RunWithLoader(
        context.Background(),
        "123",
        fakeLoader,
    )

    require.NoError(t, err)
    assert.Equal(t, "rethinking-idioms--go-constructor", result)
}
  • Litter — The Slugs Team updated their code so that they are a.) able to test their code and b.) are not testing the Medium Team’s code, however now the Slugs altered their code for the sheer purpose of testing their own functionality. While I agree it’s important to write code in order for it to be testable, to me, there is a fair amount of unnecessary bloat added and for no obvious or apparent reason. The Slugs are forced to pollute their library to test their code and their code only. Without a comment or context about the tests in cmd_test.go, the code no longer reads like “well-written prose” (another shout out to Uncle Bob).

Consideration: Mockable Constructor

When I started writing Node.js, I felt like I was coding in the wild west: so much flexibility, hard to trace, event loops, prototypical inheritance. Despite these new challenges, there was one pattern that made testing so much easier for me: the Module Pattern and being able to mock the require() function. Consider a similar example written by the aforementioned teams, except in Node.js:

// medium_article.js
class MediumArticle {
  constructor(id) {
    this.id = id;
    const apiArticle = api.GetArticle(this.id);
    this.title = apiArticle.title;
  }

  getTitle() {
    return this.title;
  }
}

// slugify.js
function slugifyTitle(titled) {
  return titled
    .getTitle()
    .replace(/[^a-zA-Z0-9]+/g, "-")
    .toLowerCase();
}

// cmd.js
const { MediumArticle } = require("./article.js")
const slugifyTitle = require("./slugify.js")

function run() {
  const id = process.argv[2];
  const article = new Article(id)
  return slugifyTitle(article)
}

Notice that the Slugs’ cmd.js is loading medium_article.js using the require() function. In this particular flavor of javascript, all external modules are loaded using the require() function. Since require is defined as a prototype property opposed to a global function, the Slugs can mock the M&A Team’s article.js without changing a single line of their production code in slugify.js:

// cmd.test.js
const mockArticle = {
    getTitle: () => "Rethinking Idioms: Go Constructor"
};

Module.prototype.require = function(id) {
    if (id === "./medium_article.js") {
        return () => mockArticle;
    }
    return originalRequire.apply(this, arguments);
};

const cmd = require("./cmd.js");
const actual = cmd.Run();

console.assert(actual === "rethinking-idioms--go-constructor");

The cmd.test.js file creates a mockArticle with a defaulted title. The Module.prototype.require assignment overrides the language’s default require() function specifying that it return a mockArticle when medium_article.js is requested. When slugify.js is required now, the first line of code actually returns the mockArticle object rather. The best part is that the implementation of the code in it’s simplest form does not change.

Revisiting the M&A Team’s implementation, let’s use a Mockable Constructor:

// medium_article.go
type MediumArticle struct {
    id    string
    title string
}

// loadArticle gets MediumArticle data from the API.
func loadArticle(ctx context.Context, id string) (MediumArticle, error) {
    // Assume api is an external package that we can't mock
    return api.GetMediumArticle(ctx, id)
}

// NewMediumArticle returns a fully initialized MediumArticle.
var NewMediumArticle = func(ctx context.Context, id string) (*MediumArticle, error) {
    article, err := loadArticle(ctx, id)
    if err != nil {
        return nil, err
    }

    return &MediumArticle{
        id:    id,
        title: article.Title,
    }, nil
}

// Title is an accessor method
func (m *MediumArticle) Title() string {
    return m.title
}

The only difference above is that NewMediumArticle is declared as an exported variable rather than an exported function. Now, this gives the Slugs access to use their original implementation in cmd.go:

// cmd.go
func Run(ctx context.Context) (string, error) {
    article, err := NewMediumArticle(ctx, os.Args[1])
    if err != nil {
        return "", err
    }

    return SlugifyTitle(article), nil
}

// cmd_test.go
type MockArticle struct{}

func (a *MockArticle) GetTitle() string {
    return "Rethinking Idioms: Go Constructor"
}

func TestRun(t *testing.T) {
    NewMediumArticle = func(title string) Article {
        return &MockArticle{}
    }

    expected := "rethinking-idioms--go-constructor"
    actual := Run()
    assert.Equal(t, expected, actual)
}

In func TestRun(t *testing.T) above, the NewMediumArticle variable is reassigned an implementation of a function that matches the same declaration, returning the MockArticle rather. The Slugs are able to use the M&A Team’s article.go library while explicitly isolating their code during testing.

The Tradeoff

There’s never a silver bullet and as with every design decision, there’s always a tradeoff.

Referencing the SOLID principles again, the O refers to the Open-Closed Principle stating that objects and entities should be open for extension and closed for modification. By loosening the reigns and using a Mockable Constructor, the M&A Team lowers their defensive coding guards. (note: the idea behind defensive coding is that the M&A Team aims to build a package so that the user, in this case the Slugs, can use without hurting themselves.) There also exists a chance that async tests run into failures due to state changes at runtime.

But its worth asking yourself, if you were on the Slugs, would you rather have the freedom to hurt yourself in exchange for clean testable code:

// Slugify Team's cmd.go if M&A Team uses a Mockable Constructor
func Run(ctx context.Context) (string, error) {
    article, err := NewMediumArticle(ctx, os.Args[1])
    if err != nil {
        return "", err
    }

    return SlugifyTitle(article), nil
}

or would you like the protections of the M&A Team in exchange for overhead:

// Slug's cmd.go with the idiomatic Constructor Function
type ArticleLoader func(ctx context.Context, id string) (*MediumArticle, error)

func RunWithLoader(
    ctx context.Context,
    id string,
    load ArticleLoader,
) (string, error) {
    article, err := load(ctx, id)
    if err != nil {
        return "", err
    }

    return SlugifyTitle(article), nil
}

func Run(ctx context.Context) (string, error) {
    return RunWithLoader(ctx, os.Args[1], NewMediumArticle)
}

Closing

We reviewed a very simple example of how using the Mockable Constructor by assigning it to an exported variable advocates testability for the consumers of your library, paying careful attention towards elegant concise code and test case isolation. This diverges from the norms of the golang practitioners, but I think it’s worth at least reevaluating said norm. After all, languages and their established conventions evolve over time due to critique and community challenges. Perhaps this is not the safest or the most elegant approach, but if you find yourself adding extra code on a dependency’s behalf, ask yourself,

Could have a Mockable Constructor made my life easier and code cleaner?


메타데이터
post_id
bd2ae6249081
slug
rethinking-idioms-go-constructor-bd2ae6249081
url
https://medium.com/@christopher.halbert/rethinking-idioms-go-constructor-bd2ae6249081
canonical_url
https://medium.com/@christopher.halbert/rethinking-idioms-go-constructor-bd2ae6249081
author_url
https://medium.com/@christopher.halbert
status
ok
fetched_at
2026-07-28 05:39:03