AlgoMaster Logo

Constructor Functions

Last Updated: May 17, 2026

11 min read

Go doesn't have constructors. There's no new Product(...) and no __init__. What Go has instead is a convention: when a struct needs setup, validation, or sensible defaults, you write a regular function whose name starts with New, like NewProduct or NewCustomer, and have it return the value. This lesson covers when to write one, what it should return, and the patterns Go programmers use to make construction safe without inventing new language features.

Why Write a Constructor at All

A struct literal already builds a value. So the obvious question is: why bother with a function that just wraps a struct literal? The answer is that struct literals can't do four things that real-world types often need.

First, they can't validate. A Product with a negative price or an empty SKU is meaningless, but Product{Price: -10} compiles fine.

Second, they can't set defaults. If you want every new order to start with status "pending" and currency "USD", the struct literal forces every caller to remember to set them.

Third, they can't initialize unexported state. If your struct hides fields behind lowercase names, callers in other packages literally cannot set those fields with a literal.

Fourth, they can't fail. A struct literal always succeeds, so there's no way to reject bad input at construction time.

A constructor is just a function that handles all four:

p2 is a Product, type-wise. But semantically it's garbage: blank SKU, blank name, negative price. The compiler is happy. The business isn't. That gap, between "the type is valid" and "the value makes sense", is what a constructor closes.

The New Naming Convention

By convention, a constructor for a type T is named NewT. So NewProduct builds a Product, NewCustomer builds a Customer, NewOrder builds an Order. The convention is so consistent in Go that anyone reading your package can guess the constructor name without checking.

When a package contains exactly one main type, the constructor sometimes drops the type name and becomes just New. The standard library uses this pattern: bytes.NewBuffer, bufio.NewReader, but also errors.New and list.New. For application code with several types per package, sticking to NewProduct, NewCustomer, etc. is clearer.

This particular constructor doesn't do anything a struct literal couldn't do. That's a fair point, and we'll come back to it later under "when you don't need a constructor". The shape, though, is the starting point for every other pattern in this lesson.

Three Common Return Styles

A constructor returns one of three shapes, and the choice tells the caller a lot about how the type is meant to be used.

Return signatureWhen to use
func NewT(...) TSmall struct, no validation, no shared state
func NewT(...) *TCaller will mutate the value or share it across functions
func NewT(...) (*T, error)Construction can fail; bad input must be rejected

Each is appropriate in different cases. The next three sections walk through them in order.

Returning a Value

The simplest constructor returns T directly. This is fine for small, immutable-ish structs where each caller gets its own copy and you don't expect any of them to be mutated through a shared reference:

a and b are independent values. Copying them, passing them to functions, or putting them in a slice all behave like any other value type. The constructor's only job here was to provide a default for Currency.

Returning a Pointer

The more common pattern in Go is to return *T. There are two reasons for this.

The first reason is mutation. If callers will modify the value after construction (adding line items to an order, updating stock on a product), they need to all see the same instance. Passing pointers around does that. Passing values around copies.

The second reason is consistency. Many types in the Go standard library expose pointer constructors (bufio.NewReader, http.NewRequest, sql.Open returns *sql.DB), and methods on those types use pointer receivers. If you intend to add pointer-receiver methods to a type, returning *T from the constructor keeps the calling code uniform.

restock modified the same Product that main is holding, because both refer to it through pointers. If NewProduct had returned a value and main had passed that value to restock, the change to Stock would have happened on a copy and been lost. The pointer return makes the intent explicit: this object has identity, and you're meant to share it.

Returning a Pointer and an Error

The third style is for when construction can genuinely fail. If NewProduct is called with a negative price, an empty SKU, or an invalid currency code, returning a half-built Product is worse than returning an error: callers will use the value and only find out about the bug much later.

The signature is (*T, error). The caller checks the error before using the value, the same way they would for any other Go function:

A few things to notice. The constructor returns nil for the pointer when there's an error, which is the standard Go idiom. Callers should never read the value when err != nil. The error messages start with "product:" to make it clear which type rejected the input; this is a common style and matters more once you have many constructors in the same package.

For wrapping a deeper error (say, the SKU validation calls another function that returned its own error), fmt.Errorf with %w is the right tool. For now, errors.New for fixed messages and fmt.Errorf for messages with values is enough.

Validation: Putting Real Rules In

The point of validation in a constructor is to make invalid values impossible. A Product that came out of NewProduct should be one you can trust without rechecking. The rules to enforce depend on the type, but a few patterns repeat:

Three small details worth pointing out. The constructor normalizes name and email by trimming whitespace before validating, so " Alice " becomes "Alice". This is a quiet feature of constructors: callers don't have to remember to trim, the constructor does it for them. The email check is intentionally simple; production code uses a library or a stricter regex. And the order of checks matters when the error messages are user-facing: validate the cheap, common things first (id, blank name) before the more expensive checks.

Defaults: Filling In What the Caller Skipped

A constructor lets you set defaults for fields the caller doesn't need to know about. The pattern is straightforward: take the fields that callers must provide as parameters, and inside the function, fill in the rest with sensible values.

The caller passed four pieces of information. The constructor turned that into a complete Order with two extra fields filled in. Now every order in the system starts with "pending" as its status, and no caller has to remember to set it.

A subtle point: defaults belong in the constructor only when they apply universally. If different callers want different "defaults", that's not a default, that's a parameter, and either the function signature should grow or you need a more flexible approach. The functional options pattern, covered shortly, is the usual answer when you have many optional fields.

Some defaults are better off as the type's zero value. If Total starts at 0.0 and Items starts as nil, the struct literal Order{ID: 4217, CustomerID: 101} already gets that for free, no constructor needed. The reason to write a constructor anyway is when the useful default isn't the zero value ("pending" is not the zero value of a string; "USD" is not the zero value either) or when validation has to run.

Hiding Unexported Fields

Lowercase field names are invisible to code outside the package. That's the main reason constructors exist for "real" types: a struct with unexported fields cannot be built with a literal from outside the package, so the constructor is the only way in.

If this code lived in a package shop and a different package tried to build an Order directly with shop.Order{ID: 1, createdAt: time.Now()}, the compiler would refuse because createdAt is unexported. The only way for outside code to get a properly stamped Order is to call shop.NewOrder. That's the leverage: the constructor becomes the single point where the type's invariants are enforced.

Functional Options (Brief)

Constructors with three or four parameters are fine. Once a type has ten optional fields and most callers only set two or three, the parameter list gets unwieldy. Go's idiomatic answer is the functional options pattern: each option is a function that mutates the value being built, and the constructor accepts a variadic list of them.

The shape looks like this:

Each caller picks the options they want. The defaults (stock 0, currency USD) stay in NewProduct. Options run in the order they're passed, so a later option can override an earlier one.

The big trade-off is that options can't easily return errors mid-construction. Real-world libraries solve this by collecting errors inside the Product and checking them after the option loop, or by having each option return an error and accumulating them. That's beyond what most application code needs. For now, the takeaway is: use functional options when you have many optional fields, especially if the type's API is going to grow. For three or four parameters, plain NewT(arg1, arg2, arg3) is shorter and clearer.

When You Don't Need a Constructor

Not every struct needs a constructor. If the type satisfies all of these, a struct literal is fine:

  • All fields are exported.
  • The zero value is usable. An empty bytes.Buffer, a zero-length slice, a 0 count, an "" string. All work without setup.
  • No validation is required. Any combination of field values is meaningful.
  • There are no defaults beyond the zero value.

Common examples are small data carriers: a Point{X, Y}, a Pair{Key, Value}, a coordinates struct passed between two functions. Writing NewPoint(x, y) for those is busywork.

Cart is the kind of struct that doesn't need a constructor. There's no invariant to protect, the zero value is meaningful (an empty cart), and any caller can read every field directly. Adding NewCart would only add noise.

The general guideline: write a constructor when you'd have to write the same setup code at every call site without it. Don't write one just because the type happens to be a struct.

Common Mistakes

A few patterns trip people up when they're first writing constructors in Go.

Panicking instead of returning an error. It's tempting to write panic("invalid price") inside the constructor and skip the error return value. Don't. panic is for unrecoverable programmer errors (a nil dereference, an impossible state). User input, parsed data, or anything from outside the program should fail with an error, not a panic. Callers can recover from errors. They usually can't recover from a panic without writing awkward defer-recover code.

Leaving fields exported when invariants exist. If NewOrder validates that Total >= 0 and stamps createdAt to the current time, but Total and createdAt are exported fields, any caller can write order.Total = -100 after construction and the invariant is broken. If a field's value has to be controlled, make it unexported and expose a method to read it.

Ignoring zero-value usability. Go's design encourages types whose zero value is useful. A sync.Mutex that hasn't been initialized still works. A strings.Builder with no setup is a valid empty builder. If your constructor's only job is "set all fields to their zero values", delete it. The zero value already does that, and the type becomes nicer to use (callers can declare it with var p Product and start using it).

NewCounter would just clutter the API. Skip it.

Returning a value when callers expect a pointer. If your type has any pointer-receiver methods, callers will be passing *T around, and returning T from the constructor forces them to write p := NewProduct(...); pp := &p everywhere. Pick one or the other based on how the type will be used in the rest of your code, and stick with it.

Putting It Together

Here's a small program that uses all three return styles in one place. NewProduct returns a value (small struct, just defaults). NewCustomer returns (*Customer, error) (real validation). NewOrder returns *Order and uses both to build a complete order.

Three constructors, three return shapes, all coexisting. NewProduct is the lightweight one (no failure mode). NewCustomer is the strict one (validate, normalize, refuse bad input). NewOrder is the orchestrator (compute the total, stamp the status, set the timestamp). This split is typical of how a real Go package ends up structured.

Summary

  • Go has no built-in constructors. The convention is a function named NewT that returns T, *T, or (*T, error), with the choice of return type signalling how the type is meant to be used.
  • Return T for small structs with no validation. Return *T when callers will share or mutate the value, or when the type has pointer-receiver methods. Return (*T, error) when bad input must be rejected at construction time.
  • Validation in a constructor is what turns a "type-valid" value into a value the rest of your code can trust. Trim, check required fields, reject negative or out-of-range numbers, and report errors with fmt.Errorf.
  • Defaults belong in the constructor only when they apply universally ("pending" status, "USD" currency). If the useful default is the zero value, you usually don't need a constructor.
  • Unexported fields force callers in other packages to go through the constructor, which is the most reliable way to keep a type's invariants intact.
  • Functional options (func(*T)) are the idiomatic way to handle many optional fields. For three or four parameters, plain positional arguments are simpler.
  • Don't panic on bad input; return an error. Don't write a constructor when the zero value is already usable. Don't leave invariant-bearing fields exported.

In the next lesson, Struct Comparison & Equality, we'll look at how == works on structs, which structs can be used as map keys, and why some struct types (those with slice or map fields) can't be compared with == at all.