27. Constructors, validation and struct design
Go has no new Person() and no constructor keyword. What it has is a
convention — a plain function named New... that returns a ready-to-use
value — plus a strong preference for making the zero value work in the first
place.
The New convention
package main
import "fmt"
type Server struct {
Host string
Port int
Timeout int
}
func NewServer(host string) *Server {
return &Server{
Host: host,
Port: 8080,
Timeout: 30,
}
}
func main() {
s := NewServer("localhost")
fmt.Printf("%+v\n", *s)
s.Port = 9090
fmt.Printf("%+v\n", *s)
}
That's the entire pattern: an ordinary function that fills in defaults and
returns a pointer. Nothing enforces it — you can still write
&Server{Host: "x"} yourself — so a constructor in Go is a convenience,
not a gate.
Naming convention: NewX in a package that offers several types
(bytes.NewReader), or plain New when the package has one obvious type
(errors.New, list.New). A package called server with a Server type
gives you server.New() — Go dislikes stutter like server.NewServer().
Constructors that can fail
When construction needs validation, return the (value, error) pair you met
in the functions module:
package main
import (
"errors"
"fmt"
)
type User struct {
Name string
Email string
Age int
}
func NewUser(name, email string, age int) (*User, error) {
if name == "" {
return nil, errors.New("name is required")
}
if age < 0 || age > 150 {
return nil, fmt.Errorf("age %d is out of range", age)
}
return &User{Name: name, Email: email, Age: age}, nil
}
func main() {
u, err := NewUser("Ada", "ada@example.com", 36)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%+v\n", *u)
if _, err := NewUser("", "x@example.com", 20); err != nil {
fmt.Println("error:", err)
}
if _, err := NewUser("Bob", "b@example.com", 200); err != nil {
fmt.Println("error:", err)
}
}
Now an invalid User can't come out of NewUser. If the type also lives in
its own package with unexported fields, NewUser becomes the only way to
build a valid one — that's how you enforce invariants in Go.
fmt.Errorf is Sprintf for errors: same formatting verbs, produces an
error.
Prefer a useful zero value
Before you write a constructor, ask whether you need one:
package main
import (
"fmt"
"strings"
)
type Buffer struct {
parts []string
}
func (b *Buffer) Add(s string) {
b.parts = append(b.parts, s)
}
func (b *Buffer) String() string {
return strings.Join(b.parts, " ")
}
func main() {
var b Buffer
b.Add("no")
b.Add("constructor")
b.Add("needed")
fmt.Println(b.String())
}
var b Buffer is immediately usable because a nil slice appends fine. This
is why sync.Mutex, bytes.Buffer and strings.Builder have no
constructors — declare one and start using it.
The design instinct: pick field types whose zero values already mean the right thing. A map field is the common exception, since a nil map panics on write — that alone is often reason enough for a constructor.
Functional options for many settings
When a type has a lot of optional configuration, Go's answer to keyword arguments is a variadic list of option functions:
package main
import "fmt"
type Client struct {
BaseURL string
Retries int
Verbose bool
}
type Option func(*Client)
func WithRetries(n int) Option {
return func(c *Client) { c.Retries = n }
}
func WithVerbose() Option {
return func(c *Client) { c.Verbose = true }
}
func NewClient(url string, opts ...Option) *Client {
c := &Client{BaseURL: url, Retries: 3}
for _, opt := range opts {
opt(c)
}
return c
}
func main() {
a := NewClient("https://api.example.com")
fmt.Printf("%+v\n", *a)
b := NewClient("https://api.example.com", WithRetries(10), WithVerbose())
fmt.Printf("%+v\n", *b)
}
Everything you've learned in the last two modules is in that snippet: a named function type, closures capturing a parameter, variadic arguments, and a pointer so the options can mutate the value being built.
It's readable at the call site (WithRetries(10) says what it does), it
extends without breaking anyone, and it's what you'll find in serious Go
libraries. Don't reach for it with two fields — reach for it when a config
struct starts sprouting booleans.
Copying and clone methods
Because assignment copies a struct, "duplicate this" is usually free — but only one level deep:
package main
import "fmt"
type Profile struct {
Name string
Tags []string
}
func (p Profile) Clone() Profile {
tags := make([]string, len(p.Tags))
copy(tags, p.Tags)
return Profile{Name: p.Name, Tags: tags}
}
func main() {
a := Profile{Name: "Ada", Tags: []string{"go", "math"}}
shallow := a
shallow.Name = "Copy"
shallow.Tags[0] = "MUTATED"
fmt.Printf("%+v\n", a)
b := a.Clone()
b.Tags[0] = "isolated"
fmt.Printf("%+v\n", a)
fmt.Printf("%+v\n", b)
}
shallow := a copied the Name but shared the Tags slice — so writing
through shallow.Tags changed a. Same slice-sharing rule from module 4,
now hiding inside a struct.
If your type contains a slice, map or pointer and callers might mutate it,
give it an explicit Clone that copies those fields properly.
Your turn
Write NewRectangle(w, h float64) (*Rectangle, error) that rejects
non-positive dimensions with the message dimensions must be positive, and
otherwise returns the rectangle. The program should print:
area: 12.00
error: dimensions must be positive
package main
import (
"errors"
"fmt"
)
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 { return r.Width * r.Height }
// write NewRectangle here
func main() {
r, err := NewRectangle(3, 4)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("area: %.2f\n", r.Area())
if _, err := NewRectangle(-1, 4); err != nil {
fmt.Println("error:", err)
}
}
package main
import (
"errors"
"fmt"
)
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func NewRectangle(w, h float64) (*Rectangle, error) {
if w <= 0 || h <= 0 {
return nil, errors.New("dimensions must be positive")
}
return &Rectangle{Width: w, Height: h}, nil
}
func main() {
r, err := NewRectangle(3, 4)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("area: %.2f\n", r.Area())
if _, err := NewRectangle(-1, 4); err != nil {
fmt.Println("error:", err)
}
}
You can now define types and give them behaviour. Next: the feature that ties Go together — interfaces.