Question

Constructors in Go: Best Practice for Struct Initialization

gooopconstructor

Question

I have a Go struct and I want it to be initialized with sensible default values.

In many languages, this is usually handled with a constructor. However, Go is not object-oriented in the traditional sense, and structs do not have built-in constructors.

I have seen the init function, but that works at the package level rather than for individual structs.

Is there a struct-level equivalent in Go?

If not, what is the accepted best practice in Go for initializing a struct with default values?

Short Answer

By the end of this page, you will understand how Go handles struct initialization without built-in constructors. You will learn the idiomatic Go approach of using constructor-style functions, when zero values are enough, and how to provide safe, readable defaults for your types.

Concept

In Go, structs do not have constructors built into the language. Instead, Go relies on a combination of:

  • zero values
  • struct literals
  • constructor-style helper functions

A zero value is the default value Go gives a type when you do not set it explicitly:

  • 0 for numbers
  • "" for strings
  • false for booleans
  • nil for slices, maps, pointers, interfaces, channels, and functions

This is a major part of Go's design. Many Go types are intentionally designed so their zero value is already useful.

For example:

type Counter struct {
    count int
}

A Counter{} is already valid because count starts at 0.

However, sometimes a type needs more than zero values to be useful. Maybe it needs:

  • a default timeout
  • a pre-created map
  • a default configuration
  • validation before use

In those cases, the idiomatic Go approach is to write a constructor-style function, usually named NewTypeName.

Example:

type Server struct {
    host    string
    port    int
    timeout int
}

func NewServer() Server {
    return Server{
        host:    "localhost",
        port:    8080,
        timeout: 30,
    }
}

This is called a constructor by convention, even though the language does not give it special syntax.

Why this matters in real programming:

  • It makes object creation consistent.
  • It lets you hide setup details.
  • It helps ensure values are valid from the start.
  • It can initialize maps, slices, or dependencies safely.
  • It gives you one clear place to change defaults later.

The init() function is different. It is only for package initialization and runs automatically when the package is loaded. It is not used to initialize each new struct instance.

Mental Model

Think of a Go struct like a paper form.

  • If the form is blank, Go still gives every field a default value. That is the zero value.
  • Sometimes a blank form is good enough.
  • Sometimes you want to hand out forms that already have common fields filled in. That is what a constructor-style function does.

So instead of the struct building itself, you create a helper function that returns a ready-to-use version of that struct.

init() is more like turning on the lights in the whole building before anyone enters. It prepares the package, not each individual form.

Take Quiz

Syntax and Examples

Basic constructor-style function

In Go, the common pattern is to define a function like NewTypeName.

package main

import "fmt"

type User struct {
    Name  string
    Admin bool
    Score int
}

func NewUser() User {
    return User{
        Name:  "Guest",
        Admin: false,
        Score: 0,
    }
}

func main() {
    user := NewUser()
    fmt.Println(user)
}

This returns a User with sensible defaults.

Returning a pointer

Sometimes constructor-style functions return a pointer, especially when:

  • the struct is large
  • methods use pointer receivers
  • you want to modify the same instance
package main

import "fmt"

type Config struct {
    Host string
    Port int
}

func NewConfig() *Config {
    return &Config{
        Host: "127.0.0.1",
        Port: 8080,
    }
}

func main() {
    cfg := NewConfig()
    fmt.Println(cfg.Host, cfg.Port)
}

Initializing maps or slices

A constructor is especially useful when fields must be allocated before use.

package main

import "fmt"

type Settings struct {
    Values map[string]string
}

func NewSettings() *Settings {
    return &Settings{
        Values: make(map[string]string),
    }
}

func main() {
    s := NewSettings()
    s.Values["theme"] = "dark"
    fmt.Println(s.Values)
}

Without make, writing into the map would panic.

Using parameters with defaults

You can also accept arguments and combine them with default values.

package main

import "fmt"

type Server struct {
    Host string
    Port int
}

func NewServer(host string) *Server {
    if host == "" {
        host = "localhost"
    }

    return &Server{
        Host: host,
        Port: 8080,
    }
}

func main() {
    s := NewServer("")
    fmt.Println(s.Host, s.Port)
}

This keeps setup logic in one place.

Step by Step Execution

Example

package main

import "fmt"

type Cache struct {
    Enabled bool
    Size    int
}

func NewCache() Cache {
    return Cache{
        Enabled: true,
        Size:    100,
    }
}

func main() {
    c := NewCache()
    fmt.Println(c.Enabled, c.Size)
}

What happens step by step

  1. The Cache struct type is defined with two fields: Enabled and Size.
  2. The function NewCache() is defined. It returns a Cache value.
  3. Inside NewCache(), a struct literal is created:
    • Enabled is set to true
    • Size is set to 100
  4. In main(), c := NewCache() calls the function.
  5. The returned Cache value is assigned to c.
  6. fmt.Println(c.Enabled, c.Size) prints:
true 100

Compare with zero values

If you wrote this instead:

var c Cache
fmt.Println(c.Enabled, c.Size)

The output would be:

false 0

That is because Go automatically gives fields their zero values when you do not set them.

Real World Use Cases

Constructor-style functions are common in Go when a type needs setup beyond zero values.

Common practical uses

  • HTTP clients with default timeouts
  • database wrappers with initialized connections or configuration
  • services with default host and port values
  • caches with preallocated maps
  • configuration structs with fallback values
  • workers with channels already created

Example: default API client

type Client struct {
    BaseURL string
    Timeout int
}

func NewClient() *Client {
    return &Client{
        BaseURL: "https://api.example.com",
        Timeout: 30,
    }
}

Example: initialized map field

type Store struct {
    items map[string]int
}

func NewStore() *Store {
    return &Store{
        items: make(map[string]int),
    }
}

This prevents runtime errors and makes the type easier to use correctly.

Take Quiz

Real Codebase Usage

In real Go codebases, developers often combine constructor-style functions with a few idiomatic patterns.

1. NewTypeName naming convention

This is the most common pattern:

func NewLogger() *Logger { ... }
func NewServer() *Server { ... }
func NewClient() *Client { ... }

It is familiar to Go developers and communicates intent clearly.

2. Zero-value-friendly design

Go developers often try to make types usable without requiring a constructor.

type Counter struct {
    n int
}

func (c *Counter) Inc() {
    c.n++
}

This works even if you declare:

var c Counter
c.Inc()

3. Guard clauses and validation

Constructors are often used to reject invalid input early.

func NewServer(port int) (*Server, error) {
    if port <= 0 {
        return nil, fmt.Errorf("invalid port: %d", port)
    }

    return &Server{Port: port}, nil
}

This is useful when not every value should be accepted.

4. Hiding internal fields

Sometimes fields are unexported so users must go through the constructor.

type TokenStore struct {
    tokens map[string]string
}

func NewTokenStore() *TokenStore {
    return &TokenStore{
        tokens: make(map[string]string),
    }
}

This helps enforce valid initialization.

5. Optional configuration patterns

A constructor can start with defaults, then apply user options.

func NewServer() *Server {
    return &Server{
        Host: "localhost",
        Port: 8080,
    }
}

Later, the caller may still modify exported fields if that fits the design.

Common Mistakes

1. Trying to use init() as a struct constructor

init() is for package setup, not per-instance setup.

Wrong idea

func init() {
    // This does not run every time a struct is created.
}

Fix

Use a normal function such as NewUser().

2. Forgetting to initialize maps

A nil map cannot be written to.

Broken code

type Settings struct {
    Values map[string]string
}

func main() {
    s := Settings{}
    s.Values["theme"] = "dark"
}

This will panic.

Fix

func NewSettings() *Settings {
    return &Settings{
        Values: make(map[string]string),
    }
}

3. Creating constructors when zero values are already enough

Not every type needs a constructor.

Unnecessary

type Counter struct {
    Count int
}

func NewCounter() Counter {
    return Counter{Count: 0}
}

This adds little value because Counter{} already works.

4. Returning a value when you really want shared mutation

If methods need to update the same instance, returning a pointer is often clearer.

func NewConfig() Config {
    return Config{Port: 8080}
}

This is not always wrong, but if your methods use pointer receivers, returning *Config may be more consistent.

5. Duplicating default values in many places

Problem

s1 := Server{Host: "localhost", Port: 8080}
s2 := Server{Host: "localhost", Port: 8080}
s3 := Server{Host: "localhost", Port: 8080}

If defaults change, you must update many call sites.

Better

func NewServer() Server {
    return Server{Host: "localhost", Port: 8080}
}

Comparisons

Struct literal vs constructor-style function

ApproachExampleBest whenProsCons
Struct literalUser{Name: "A"}Caller knows all valuesSimple and directRepeats setup logic
Zero valuevar c CounterZero value is validVery idiomaticCannot set custom defaults
Constructor-style functionNewUser()Defaults or validation are neededCentralized setupAdds one extra function

init() vs NewType()

Featureinit()NewType()
Runs automaticallyYesNo
ScopePackage-levelPer instance
Used for struct defaultsNoYes
Can accept parametersNoYes
Common useSetup package stateCreate initialized values

Return value vs pointer

Return typeExampleUse when
Valuefunc NewPoint() PointThe struct is small and copied safely
Pointerfunc NewServer() *ServerThe struct is large, mutable, or uses pointer receivers
Take Quiz

Cheat Sheet

Quick rules

  • Go has no built-in constructors for structs.
  • Use a constructor-style function like NewTypeName.
  • Prefer designing types so the zero value is useful when possible.
  • Use init() only for package initialization, not struct instances.
  • Use constructors when you need:
    • default values
    • validation
    • initialized maps, slices, or channels
    • hidden setup logic

Common patterns

func NewUser() User {
    return User{Name: "Guest"}
}

func NewConfig() *Config {
    return &Config{Port: 8080}
}

func NewServer(port int) (*Server, error) {
    if port <= 0 {
        return nil, fmt.Errorf("invalid port")
    }
    return &Server{Port: port}, nil
}

Zero value reminder

var n int       // 0
var s string    // ""
var b bool      // false
var p *User     // nil
var m map[string]int // nil

Important edge case

A nil map can be read from, but writing to it causes a panic:

var m map[string]int
// m["x"] = 1 // panic

Initialize it first:

m = make(map[string]int)

FAQ

Is there a constructor keyword in Go?

No. Go does not have a constructor keyword. The usual approach is a normal function such as NewUser().

Should every struct in Go have a constructor?

No. If the zero value is already useful, a constructor is often unnecessary.

What is the idiomatic name for a constructor in Go?

The common naming convention is NewTypeName, such as NewClient or NewServer.

When should a Go constructor return a pointer?

Usually when the struct is mutable, large, or commonly used through pointer receiver methods.

Can I use init() to set default values for each struct instance?

No. init() runs once for package initialization, not every time you create a struct.

How do I enforce valid initialization in Go?

Use a constructor-style function that validates inputs and returns either a valid struct or an error.

Why are zero values important in Go?

They allow many types to work immediately without extra setup, which keeps code simple and idiomatic.

Take Quiz

Mini Project

Description

Build a small Go configuration type for an application server. This project demonstrates when constructor-style functions are useful: setting defaults, initializing internal data, and making the type easy to use safely.

Goal

Create a ServerConfig type that starts with sensible defaults and can be used immediately without manual setup.

Requirements

  • Create a ServerConfig struct with fields for host, port, debug mode, and headers.
  • Write a constructor-style function that returns a ready-to-use config with sensible defaults.
  • Initialize any map fields properly so writing to them does not panic.
  • In main, create a config using the constructor and customize one or two fields.
  • Print the final config values to confirm the defaults and custom changes are applied.
Take Quiz

Keep learning

Related questions

View more