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:
0for numbers""for stringsfalsefor booleansnilfor 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.
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
- The
Cachestruct type is defined with two fields:EnabledandSize. - The function
NewCache()is defined. It returns aCachevalue. - Inside
NewCache(), a struct literal is created:Enabledis set totrueSizeis set to100
- In
main(),c := NewCache()calls the function. - The returned
Cachevalue is assigned toc. 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.
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
| Approach | Example | Best when | Pros | Cons |
|---|---|---|---|---|
| Struct literal | User{Name: "A"} | Caller knows all values | Simple and direct | Repeats setup logic |
| Zero value | var c Counter | Zero value is valid | Very idiomatic | Cannot set custom defaults |
| Constructor-style function | NewUser() | Defaults or validation are needed | Centralized setup | Adds one extra function |
init() vs NewType()
| Feature | init() | NewType() |
|---|---|---|
| Runs automatically | Yes | No |
| Scope | Package-level | Per instance |
| Used for struct defaults | No | Yes |
| Can accept parameters | No | Yes |
| Common use | Setup package state | Create initialized values |
Return value vs pointer
| Return type | Example | Use when |
|---|---|---|
| Value | func NewPoint() Point | The struct is small and copied safely |
| Pointer | func NewServer() *Server | The struct is large, mutable, or uses pointer receivers |
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.
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
ServerConfigstruct 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.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.