Skip to main content

Constructor Patterns

Go has no classes or constructors. Instead, it uses the New function pattern and the Functional Options Pattern to create objects safely and flexibly. These patterns are used throughout Go's standard library and most major Go open-source projects.

Basic Struct Literals​

The simplest approach is to create structs directly with struct literals.

package main

import "fmt"

type Point struct {
X, Y float64
}

type Color struct {
R, G, B uint8
}

type Pixel struct {
Point
Color
}

func main() {
// Initialize by order without field names (not recommended β€” breaks when fields are added)
p1 := Point{3.0, 4.0}

// Explicit field names (recommended)
p2 := Point{X: 3.0, Y: 4.0}

// Partial initialization (rest are zero values)
p3 := Point{X: 5.0} // Y is 0.0

fmt.Println(p1, p2, p3)

// Embedded struct initialization
px := Pixel{
Point: Point{X: 10, Y: 20},
Color: Color{R: 255, G: 0, B: 0},
}
fmt.Println(px)
}

New Function Pattern​

When you need validation or default values, implement a constructor as a New function.

package main

import (
"errors"
"fmt"
"strings"
)

type User struct {
id int // lowercase = unexported field
name string
email string
isActive bool
}

// Constructor function β€” includes validation
func NewUser(id int, name, email string) (*User, error) {
if id <= 0 {
return nil, errors.New("ID must be positive")
}
name = strings.TrimSpace(name)
if name == "" {
return nil, errors.New("name is required")
}
if !strings.Contains(email, "@") {
return nil, fmt.Errorf("invalid email: %s", email)
}

return &User{
id: id,
name: name,
email: strings.ToLower(email),
isActive: true, // default value
}, nil
}

// Getter methods (access unexported fields)
func (u *User) ID() int { return u.id }
func (u *User) Name() string { return u.name }
func (u *User) Email() string { return u.email }
func (u *User) IsActive() bool { return u.isActive }

func (u *User) Deactivate() {
u.isActive = false
}

func (u User) String() string {
status := "active"
if !u.isActive {
status = "inactive"
}
return fmt.Sprintf("User{id=%d, name=%s, email=%s, status=%s}",
u.id, u.name, u.email, status)
}

func main() {
// Successful creation
user, err := NewUser(1, "John Doe", "john@example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(user)

// Validation failures
_, err = NewUser(0, "Test", "invalid-email")
fmt.Println("Error:", err) // Error: ID must be positive

_, err = NewUser(2, "", "test@test.com")
fmt.Println("Error:", err) // Error: name is required

user.Deactivate()
fmt.Println(user)
}

Functional Options Pattern​

The most flexible pattern when there are many parameters or optional configurations. Widely used in major Go libraries like grpc-go, zap, and cobra.

package main

import (
"fmt"
"time"
)

type Server struct {
host string
port int
timeout time.Duration
maxConns int
readTimeout time.Duration
writeTimeout time.Duration
tlsEnabled bool
logLevel string
}

// Option type: a function that modifies Server
type Option func(*Server)

// Apply defaults then options
func NewServer(host string, port int, opts ...Option) *Server {
// Sensible defaults
s := &Server{
host: host,
port: port,
timeout: 30 * time.Second,
maxConns: 100,
readTimeout: 10 * time.Second,
writeTimeout: 10 * time.Second,
logLevel: "info",
}
// Apply options in order
for _, opt := range opts {
opt(s)
}
return s
}

// Option constructor functions
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}

func WithMaxConns(n int) Option {
return func(s *Server) {
s.maxConns = n
}
}

func WithReadTimeout(d time.Duration) Option {
return func(s *Server) {
s.readTimeout = d
}
}

func WithWriteTimeout(d time.Duration) Option {
return func(s *Server) {
s.writeTimeout = d
}
}

func WithTLS() Option {
return func(s *Server) {
s.tlsEnabled = true
}
}

func WithLogLevel(level string) Option {
return func(s *Server) {
s.logLevel = level
}
}

func (s *Server) Start() {
scheme := "http"
if s.tlsEnabled {
scheme = "https"
}
fmt.Printf("Server started: %s://%s:%d\n", scheme, s.host, s.port)
fmt.Printf(" Timeout: %v, MaxConns: %d\n", s.timeout, s.maxConns)
fmt.Printf(" ReadTimeout: %v, WriteTimeout: %v\n", s.readTimeout, s.writeTimeout)
fmt.Printf(" LogLevel: %s\n", s.logLevel)
}

func main() {
// Use defaults only
s1 := NewServer("localhost", 8080)
s1.Start()

fmt.Println()

// With options
s2 := NewServer("0.0.0.0", 443,
WithTLS(),
WithTimeout(60*time.Second),
WithMaxConns(1000),
WithReadTimeout(30*time.Second),
WithWriteTimeout(30*time.Second),
WithLogLevel("debug"),
)
s2.Start()
}

Functional Options with Validation​

package main

import (
"errors"
"fmt"
)

type DatabaseConfig struct {
host string
port int
dbName string
user string
password string
maxOpen int
maxIdle int
sslMode string
}

type DBOption func(*DatabaseConfig) error

func NewDatabaseConfig(host, dbName, user string, opts ...DBOption) (*DatabaseConfig, error) {
if host == "" || dbName == "" || user == "" {
return nil, errors.New("host, dbName, user are required")
}

cfg := &DatabaseConfig{
host: host,
port: 5432,
dbName: dbName,
user: user,
maxOpen: 25,
maxIdle: 5,
sslMode: "disable",
}

for _, opt := range opts {
if err := opt(cfg); err != nil {
return nil, err
}
}

return cfg, nil
}

func WithPort(port int) DBOption {
return func(cfg *DatabaseConfig) error {
if port < 1 || port > 65535 {
return fmt.Errorf("invalid port: %d", port)
}
cfg.port = port
return nil
}
}

func WithPassword(password string) DBOption {
return func(cfg *DatabaseConfig) error {
cfg.password = password
return nil
}
}

func WithPoolSize(maxOpen, maxIdle int) DBOption {
return func(cfg *DatabaseConfig) error {
if maxIdle > maxOpen {
return errors.New("maxIdle cannot exceed maxOpen")
}
cfg.maxOpen = maxOpen
cfg.maxIdle = maxIdle
return nil
}
}

func WithSSL() DBOption {
return func(cfg *DatabaseConfig) error {
cfg.sslMode = "require"
return nil
}
}

func (cfg *DatabaseConfig) DSN() string {
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
cfg.host, cfg.port, cfg.dbName, cfg.user, cfg.password, cfg.sslMode)
}

func main() {
cfg, err := NewDatabaseConfig("localhost", "mydb", "admin",
WithPassword("secret"),
WithPoolSize(50, 10),
WithSSL(),
)
if err != nil {
fmt.Println("Config error:", err)
return
}
fmt.Println("DSN:", cfg.DSN())

// Validation failure
_, err = NewDatabaseConfig("localhost", "mydb", "admin",
WithPort(99999),
)
fmt.Println("Error:", err)

_, err = NewDatabaseConfig("localhost", "mydb", "admin",
WithPoolSize(5, 10), // maxIdle > maxOpen
)
fmt.Println("Error:", err)
}

Builder Pattern​

The builder pattern with method chaining is also frequently used in Go.

package main

import (
"fmt"
"strings"
)

type EmailBuilder struct {
from string
to []string
cc []string
subject string
body string
html bool
}

func NewEmailBuilder(from string) *EmailBuilder {
return &EmailBuilder{from: from}
}

func (b *EmailBuilder) To(addresses ...string) *EmailBuilder {
b.to = append(b.to, addresses...)
return b
}

func (b *EmailBuilder) CC(addresses ...string) *EmailBuilder {
b.cc = append(b.cc, addresses...)
return b
}

func (b *EmailBuilder) Subject(subject string) *EmailBuilder {
b.subject = subject
return b
}

func (b *EmailBuilder) Body(body string) *EmailBuilder {
b.body = body
return b
}

func (b *EmailBuilder) HTML() *EmailBuilder {
b.html = true
return b
}

type Email struct {
From string
To []string
CC []string
Subject string
Body string
IsHTML bool
}

func (b *EmailBuilder) Build() (*Email, error) {
var errs []string
if len(b.to) == 0 {
errs = append(errs, "no recipients")
}
if b.subject == "" {
errs = append(errs, "no subject")
}
if b.body == "" {
errs = append(errs, "no body")
}
if len(errs) > 0 {
return nil, fmt.Errorf("email build failed: %s", strings.Join(errs, "; "))
}

return &Email{
From: b.from,
To: b.to,
CC: b.cc,
Subject: b.subject,
Body: b.body,
IsHTML: b.html,
}, nil
}

func main() {
email, err := NewEmailBuilder("sender@example.com").
To("alice@example.com", "bob@example.com").
CC("manager@example.com").
Subject("Hello from Go!").
Body("<h1>Learning Go is fun</h1>").
HTML().
Build()

if err != nil {
fmt.Println("Error:", err)
return
}

fmt.Printf("From: %s\n", email.From)
fmt.Printf("To: %s\n", strings.Join(email.To, ", "))
fmt.Printf("CC: %s\n", strings.Join(email.CC, ", "))
fmt.Printf("Subject: %s\n", email.Subject)
fmt.Printf("HTML: %v\n", email.IsHTML)

// Validation failure
_, err = NewEmailBuilder("sender@example.com").
Subject("Title").
Build()
fmt.Println("Error:", err)
}

Singleton Pattern​

Used for global config, database connections, and other single-instance resources.

package main

import (
"fmt"
"sync"
)

type AppConfig struct {
Environment string
Debug bool
Version string
}

var (
instance *AppConfig
once sync.Once
)

func GetConfig() *AppConfig {
once.Do(func() {
// In practice, read from file/environment variables
instance = &AppConfig{
Environment: "development",
Debug: true,
Version: "1.0.0",
}
fmt.Println("Config initialized")
})
return instance
}

func main() {
// Multiple calls β€” initialization happens only once
cfg1 := GetConfig() // prints "Config initialized"
cfg2 := GetConfig() // no output
cfg3 := GetConfig() // no output

fmt.Println(cfg1 == cfg2) // true (same pointer)
fmt.Println(cfg2 == cfg3) // true
fmt.Printf("Environment: %s, Version: %s\n", cfg1.Environment, cfg1.Version)
}

Pattern Selection Guide

SituationRecommended Pattern
Few fields, all requiredStruct literal or New function
Validation neededNew function (returns error)
Many optional settingsFunctional Options Pattern
Complex step-by-step constructionBuilder Pattern
Single global instanceSingleton (sync.Once)