Constructor Functions in Go and the New Convention
Go has no constructors, only functions named New that return a ready value. See the naming rules, validation with errors and when to skip one.

Go has no constructor keyword, no new on a type and no initializer blocks. A struct is created with a literal, and the compiler fills every field with its zero value. When that is not enough, the community convention is a plain function that returns a ready value, and the convention is so consistent that you can predict the name before you read the documentation. This how-to covers the naming rules, the shape of a good constructor, the validation question that decides whether it returns an error and the cases where you should not write one at all. It builds on structs and the useful zero value, leads into functional options, and sits in the Go basics path.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
A constructor that validates
This server type has a required address, an optional timeout and a dependency that should not be nil. The constructor checks them, applies a default, and returns an error for bad input:
package main
import (
"errors"
"fmt"
"log/slog"
"os"
"strings"
"time"
)
type Server struct {
addr string
timeout time.Duration
logger *slog.Logger
}
const DefaultTimeout = 30 * time.Second
func NewServer(addr string, timeout time.Duration, logger *slog.Logger) (*Server, error) {
if strings.TrimSpace(addr) == "" {
return nil, errors.New("server: address is required")
}
if timeout < 0 {
return nil, fmt.Errorf("server: timeout %v is negative", timeout)
}
if timeout == 0 {
timeout = DefaultTimeout
}
if logger == nil {
logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
}
return &Server{addr: addr, timeout: timeout, logger: logger}, nil
}
func (s *Server) Describe() string {
return fmt.Sprintf("%s (timeout %v)", s.addr, s.timeout)
}
func main() {
s, err := NewServer("localhost:8080", 0, nil)
fmt.Println(s.Describe(), err)
s.logger.Info("ready", "addr", s.addr)
_, err = NewServer("", time.Second, nil)
fmt.Println(err)
_, err = NewServer("localhost:1", -time.Second, nil)
fmt.Println(err)
}localhost:8080 (timeout 30s) <nil>
level=INFO msg=ready addr=localhost:8080
server: address is required
server: timeout -1s is negativeThe valid call got the default timeout, 30s, because a zero timeout is documented as "use the default", and a logger was supplied for it, since the caller passed nil. The two invalid calls returned errors and no server, which is the property that matters: an invalid *Server never exists, so no method needs to defend against one. The error messages start with the package or type name and use lowercase with no trailing punctuation, matching the naming conventions. The log line used log/slog with the timestamp removed so that the output is the same on each run.
The naming rule
The name follows how many types the package exports. A package with one main type uses New:
| Package | Constructor | Reads as |
|---|---|---|
errors | errors.New | A new error |
bufio | bufio.NewReader, bufio.NewWriter | Several types, so the type name is in the function name |
list | list.New | The package's single main type |
http | http.NewRequest, http.NewServeMux | Several types |
strings | strings.NewReader, strings.NewReplacer | Several types |
When the package's type is named after the package, as in cache.Cache, the constructor is cache.New() and not cache.NewCache(), since the repeated word is the stutter package naming tells you to avoid. When a constructor takes a different source, the convention is NewFromFile, NewWithConfig or Open for things that acquire a resource, and Parse for building from text.
Returning a pointer or a value
Return *T when the type has identity or state that methods change, a server, a pool, anything with a mutex, as pointer or value receivers explains. Return T for small immutable values: time.Date, url.Values, a Point. The choice affects whether the zero value of the return is meaningful on an error: with *T it is nil, and callers check the error before using the pointer, and with T you return the zero value alongside the error. The measurement in returning a pointer or a value shows that for a small struct the value return is the cheaper one.
Test the validation
The constructor is where invalid states are rejected, so test it with the inputs it should reject:
package server
import (
"errors"
"fmt"
"strings"
"time"
)
type Server struct {
addr string
timeout time.Duration
}
func New(addr string, timeout time.Duration) (*Server, error) {
if strings.TrimSpace(addr) == "" {
return nil, errors.New("server: address is required")
}
if timeout < 0 {
return nil, fmt.Errorf("server: timeout %v is negative", timeout)
}
if timeout == 0 {
timeout = 30 * time.Second
}
return &Server{addr: addr, timeout: timeout}, nil
}
func (s *Server) Timeout() time.Duration { return s.timeout }package server
import (
"testing"
"time"
)
func TestNew(t *testing.T) {
cases := []struct {
name string
addr string
timeout time.Duration
want time.Duration
wantErr bool
}{
{"default timeout", "a:1", 0, 30 * time.Second, false},
{"explicit timeout", "a:1", 2 * time.Second, 2 * time.Second, false},
{"missing address", " ", time.Second, 0, true},
{"negative timeout", "a:1", -1, 0, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
s, err := New(c.addr, c.timeout)
if (err != nil) != c.wantErr {
t.Fatalf("err = %v, wantErr %v", err, c.wantErr)
}
if err == nil && s.Timeout() != c.want {
t.Errorf("timeout = %v, want %v", s.Timeout(), c.want)
}
})
}
}=== RUN TestNew
=== RUN TestNew/default_timeout
=== RUN TestNew/explicit_timeout
=== RUN TestNew/missing_address
=== RUN TestNew/negative_timeout
--- PASS: TestNew (0.00s)
--- PASS: TestNew/default_timeout (0.00s)
--- PASS: TestNew/explicit_timeout (0.00s)
--- PASS: TestNew/missing_address (0.00s)
--- PASS: TestNew/negative_timeout (0.00s)
PASS
ok example.test/lab 1.268sThe table covers the default, an explicit value and the two rejections, and the unexported fields are read through a method. The test for the constructor of a type with many options grows quickly, which is the signal to move to functional options.
Open, Dial and Start: constructors that acquire something
When building the value takes a resource, a file, a network connection, a goroutine, the convention is to say so in the name. os.Open, net.Dial, sql.Open and http.ListenAndServe all return a value that holds something the caller must release, and the matching release method is almost always Close or Stop. The name is a warning, since New implies a cheap, side-effect-free value. Here is the shape in miniature:
package main
import (
"errors"
"fmt"
)
type Conn struct {
addr string
closed bool
}
func Dial(addr string) (*Conn, error) {
if addr == "" {
return nil, errors.New("dial: empty address")
}
fmt.Println("opened", addr)
return &Conn{addr: addr}, nil
}
func (c *Conn) Close() error {
if c.closed {
return errors.New("close: already closed")
}
c.closed = true
fmt.Println("closed", c.addr)
return nil
}
func main() {
conn, err := Dial("db.example.test:5432")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
_, err = Dial("")
fmt.Println(err)
}opened db.example.test:5432
dial: empty address
closed db.example.test:5432The defer goes right after the error check, so the connection is closed however the function exits, and the failed Dial returns no value to close. The constructor either hands back something ready and owned, or an error and nothing. A zero value of Conn could not be useful, since there is no connection behind it, so the type has no usable zero state and the documentation should say so. Defer and the order things run in covers why the pairing is written this way.
Exported fields with a Validate method
There is a second way to enforce rules, and it suits configuration. Export the fields, let callers fill them in with a literal, and give the type a Validate() error method that the consumer calls once:
type Config struct {
Addr string
Timeout time.Duration
}
func (c Config) Validate() error {
if c.Addr == "" {
return errors.New("config: address is required")
}
return nil
}The trade-off is that an invalid Config can exist, in the window between construction and the check, and the benefit is that the type is a plain data structure that decodes from JSON or YAML and can be compared and copied. For a type that is data, this is the better design. For a type with behavior and invariants, the unexported fields and a constructor give a stronger guarantee. Many programs have both: a Config that is data and validated, passed to a New that builds the stateful Server from it. Keep the validation in one place, whichever style you choose, because two copies of a rule drift apart, and a rule that lives in the constructor and in a Validate method is a bug waiting for a change to only one of them.
When not to write one
If every field's zero value is already right, var x T or a literal is the constructor, and a NewT() that returns &T{} is noise. If the only job of the function is to copy arguments into fields, export the fields and use a literal. Constructors also hide the types they build from go doc unless they are exported next to the type, so keep them adjacent in the file. And do not write a constructor that does I/O or starts goroutines without saying so in its name, Open, Start or Dial, because callers expect New to be cheap and side-effect free. The wider design question, when to prefer a type that needs no setup, is in make the zero value useful, and the conventions come from Effective Go on composite literals and constructors and the Go Code Review Comments.


