Introduction
Go does not have a special constructor keyword or a new class mechanism like Java or Python. Instead, the idiomatic way to initialize complex structs is through factory functions, typically named New or New[Type]. This pattern gives you full control over validation, defaults, and returned types.
Key Concepts
- Factory Function: A regular function that creates and returns an initialized struct, conventionally named
NewTypeName. - Returning Pointers: Factories typically return
*Tto avoid large copies and allow nil error returns. - Default Values: Factories set sensible defaults for fields the caller does not need to specify.
- Functional Options: An advanced pattern for configurable constructors using variadic option functions.
Real World Context
Every major Go project uses this pattern. The standard library has http.NewServeMux(), bufio.NewReader(), and errors.New(). In application code, constructors validate configuration, establish database connections, and wire up dependencies. When a package exports only one main type, the convention is to name the factory simply New().
Deep Dive
The typical constructor returns a pointer and optionally an error.
gotype Server struct { Port int DB *sql.DB } func NewServer(port int) (*Server, error) { if port <= 0 { return nil, errors.New("invalid port") } return &Server{ Port: port, DB: nil, }, nil }
Returning a pointer is idiomatic because it avoids copying large structs, allows nil to signal failure, and matches the convention of pointer receiver methods.
Setting Default Values
Factories are the right place to assign sensible defaults.
gofunc NewConfig() *Config { return &Config{ Timeout: 30 * time.Second, MaxRetries: 3, Debug: false, } }
Callers can override specific fields after construction when needed.
Naming Conventions
The standard library establishes a clear naming pattern:
NewTypeNamewhen the package exports multiple types (http.NewServeMux()).Newwhen the package exports one primary type (errors.New(),ring.New()).
Following this convention makes your API instantly familiar to other Go developers.
Common Pitfalls
- Skipping validation in constructors — Letting invalid state pass through the factory pushes errors deeper into the program where they are harder to debug.
- Returning values instead of pointers for large structs — This causes unnecessary copying and prevents nil-based error signaling.
Enhanced new() in Go 1.26\n\nGo 1.26 introduced an important quality-of-life improvement: the new() builtin now accepts expressions to initialize the value. Previously, creating a pointer to a simple value required a temporary variable:\n\ngo\n// Before Go 1.26 — needed a temp variable\nage := 25\nuser := User{Age: &age}\n\n\nNow you can write this directly:\n\ngo\n// Go 1.26+ — new() accepts expressions\nuser := User{Age: new(25)}\nuser2 := User{Age: new(yearsSince(born))}\n\n\nThis eliminates the common annoyance of needing temporary variables just to take the address of a value, especially useful for optional pointer fields in JSON structs and protobuf messages.\n\n## Best Practices
- Always validate inputs in the factory — Return an error early if the configuration is invalid rather than panicking later.
- Follow the
NewTypeNamenaming convention — Consistency with the standard library makes your API predictable and discoverable.
Summary
- Go uses factory functions (
NewTypeName) instead of class constructors. - Factories return pointers and optional errors for clean initialization.
- Use factories to set default values and validate inputs.
- Follow standard library naming:
NewXfor single-type packages,NewTypeNamefor multi-type packages. - The functional options pattern extends constructors for complex configuration needs.
Code Examples
package main
type File struct {
Name string
Mode os.FileMode
}
func NewFile(name string) *File {
return &File{
Name: name,
Mode: 0644, // Default permissions
}
}
func main() {
f := NewFile("data.txt")
fmt.Println(f.Name)
}