Question
In Go, how can you set default values for struct fields, and what are the common ways to initialize structs when you want values other than Go's zero values?
For example, if a struct has several fields, how should it be created so that some fields start with meaningful defaults instead of the language-provided defaults? I have seen multiple approaches and want to understand the standard patterns and trade-offs.
Short Answer
By the end of this page, you will understand how Go initializes structs by default, what zero values are, and how to provide your own defaults using literals, constructor functions, and option-style patterns. You will also see when each approach is appropriate and how this is commonly handled in real Go codebases.
Concept
In Go, every variable has a zero value when it is created without explicit initialization. Struct fields follow the same rule.
For example:
intbecomes0stringbecomes""boolbecomesfalse- pointers, slices, maps, functions, and interfaces become
nil
That means Go does not have built-in support for declaring custom default field values directly inside a struct type definition.
type ServerConfig struct {
Host string
Port int
Debug bool
}
If you create this struct with:
var cfg ServerConfig
then the values are:
Host = ""Port = 0Debug = false
If you want different defaults such as Host = "localhost" or Port = 8080, you must set them yourself.
This matters because real applications often need sensible starting values:
- a server should use a common port
- a timeout should have a safe duration
- a feature flag may default to disabled
- configuration objects should be valid immediately after creation
In Go, the usual way to do this is to use one of these patterns:
- Struct literal with explicit values
- Constructor function such as
NewConfig() - Constructor with overrides
- Functional options pattern for more flexible configuration
The key idea is simple: Go gives you zero values automatically, and you provide custom defaults through initialization code, not through the struct definition itself.
Mental Model
Think of a Go struct like a new form with blank or standard system-filled fields.
- Go automatically fills every field with its zero value.
- If you want more useful starting values, you create the form through a helper that fills in the common defaults first.
So instead of thinking, "How do I put defaults inside the struct type?", think:
"What is the safest way to create this struct so it starts in a valid state?"
A constructor function is like a factory worker who prepares the object before handing it to you.
- Go zero values = the factory's raw material
- constructor = the worker applying your preferred defaults
- caller overrides = custom adjustments for a specific use case
Syntax and Examples
Zero-value initialization
If you declare a struct without assigning field values, Go uses zero values.
package main
import "fmt"
type User struct {
Name string
Age int
Active bool
}
func main() {
var u User
fmt.Printf("%+v\n", u)
}
Output:
{Name: Age:0 Active:false}
Struct literal with explicit values
You can set values directly when creating the struct.
u := User{
Name: "Alice",
Age: 25,
Active: true,
}
This is best when:
- you are creating the value in one place
- all important values are already known
- you do not need reusable defaults
Constructor function for defaults
A common Go pattern is to write a constructor function.
package main
import "fmt"
type ServerConfig struct {
Host string
Port int
Debug bool
}
func NewServerConfig() ServerConfig {
return ServerConfig{
Host: "localhost",
Port: 8080,
Debug: false,
}
}
func main() {
cfg := NewServerConfig()
fmt.Printf("%+v\n", cfg)
}
This is useful because:
- defaults are defined in one place
- code is easier to reuse
- new instances start in a sensible state
Constructor with overrides
Sometimes you want defaults plus a few custom changes.
package main
import "fmt"
type ServerConfig struct {
Host string
Port int
Debug bool
}
func NewServerConfig(host string, port int) ServerConfig {
cfg := ServerConfig{
Host: "localhost",
Port: 8080,
Debug: false,
}
if host != "" {
cfg.Host = host
}
if port != 0 {
cfg.Port = port
}
return cfg
}
func main() {
cfg := NewServerConfig("example.com", 9090)
fmt.Printf("%+v\n", cfg)
}
Functional options pattern
This pattern is common when a struct has many optional settings.
package main
import "fmt"
type Server struct {
Host string
Port int
Debug bool
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.Host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.Port = port
}
}
func WithDebug(debug bool) Option {
return func(s *Server) {
s.Debug = debug
}
}
func NewServer(opts ...Option) *Server {
s := &Server{
Host: "localhost",
Port: 8080,
Debug: false,
}
for _, opt := range opts {
opt(s)
}
return s
}
func main() {
srv := NewServer(WithPort(9000), WithDebug(true))
fmt.Printf("%+v\n", *srv)
}
This keeps defaults in one place while allowing flexible customization.
Step by Step Execution
Consider this example:
package main
import "fmt"
type Config struct {
Host string
Port int
}
func NewConfig() Config {
return Config{
Host: "localhost",
Port: 8080,
}
}
func main() {
cfg := NewConfig()
fmt.Println(cfg.Host)
fmt.Println(cfg.Port)
}
Step-by-step
-
type Config struct { ... }- Defines a struct type with two fields:
HostandPort.
- Defines a struct type with two fields:
-
func NewConfig() Config- Declares a function that returns a
Configvalue.
- Declares a function that returns a
-
return Config{ Host: "localhost", Port: 8080 }- Creates a new struct literal.
- Assigns custom values instead of leaving fields at zero values.
-
cfg := NewConfig()- Calls the constructor function.
cfgnow containsHost = "localhost"andPort = 8080.
-
fmt.Println(cfg.Host)- Prints
localhost.
- Prints
-
fmt.Println(cfg.Port)- Prints
8080.
- Prints
What would happen without the constructor?
If you wrote:
var cfg Config
then:
cfg.Hostwould be""cfg.Portwould be0
That is valid Go, but often not the behavior you want for application settings.
Real World Use Cases
Custom defaults for structs are common in many kinds of Go programs.
Application configuration
type AppConfig struct {
Env string
Port int
LogJSON bool
}
Defaults might be:
Env: "development"Port: 3000LogJSON: false
HTTP servers and clients
A server or client may need default:
- timeout values
- base URLs
- retry counts
- ports
Database settings
A database config struct may define defaults for:
- max open connections
- idle timeout
- connection lifetime
CLI tools
Command-line tools often use defaults unless the user passes flags.
Example:
- default output format:
text - default config file path
- default verbosity level
Data processing jobs
Batch jobs may use default:
- chunk sizes
- worker counts
- retry policies
In all of these cases, the struct itself does not store custom defaults automatically. The creation logic does.
Real Codebase Usage
In real Go codebases, developers usually choose one of a few practical patterns.
1. Constructor returning a ready-to-use value
func NewConfig() Config {
return Config{
Port: 8080,
}
}
Use this when:
- defaults are simple
- the struct should always start valid
- you want one clear creation path
2. Constructor returning a pointer
func NewConfig() *Config {
return &Config{
Port: 8080,
}
}
This is common when:
- the struct is large
- methods use pointer receivers
- the value will be modified after creation
3. Defaults plus validation
A constructor often sets defaults and then validates input.
func NewConfig(port int) (Config, error) {
cfg := Config{Port: 8080}
if port != 0 {
cfg.Port = port
}
if cfg.Port < 1 || cfg.Port > 65535 {
return Config{}, fmt.Errorf("invalid port")
}
return cfg, nil
}
4. Guard clauses and normalization
Real projects often clean inputs before storing them.
func NewUser(name string) User {
if name == "" {
name = "guest"
}
return User{Name: name}
}
5. Functional options for growing APIs
When a struct gets more fields over time, function parameters become awkward. Options keep the API readable.
srv := NewServer(WithHost("0.0.0.0"), WithDebug(true))
6. Config loaded from files or environment
A common pattern is:
- create a struct with defaults
- override from file
- override from environment variables
- validate the final result
That layered approach is very common in production systems.
Common Mistakes
1. Expecting custom defaults in the struct definition
This is not valid Go:
type Config struct {
Host string = "localhost"
Port int = 8080
}
Go struct fields cannot be assigned default values inside the type definition.
2. Confusing zero values with missing values
Sometimes 0 or "" is a real value, not just a missing value.
func NewConfig(port int) Config {
cfg := Config{Port: 8080}
if port != 0 {
cfg.Port = port
}
return cfg
}
This treats 0 as "not provided." That may be okay, but only if 0 is never meaningful in your design.
To avoid confusion, you can use pointers or option functions when you need to distinguish "unset" from an intentional zero.
3. Repeating defaults in many places
Broken approach:
cfg1 := Config{Host: "localhost", Port: 8080}
cfg2 := Config{Host: "localhost", Port: 8080}
cfg3 := Config{Host: "localhost", Port: 8080}
This is hard to maintain. If the default port changes, you must update multiple places.
Better:
func NewConfig() Config {
return Config{Host: "localhost", Port: 8080}
}
4. Forgetting to initialize maps or slices when needed
A nil slice is often okay, but a nil map cannot be written to.
Broken code:
type Settings struct {
Labels map[string]string
}
func NewSettings() Settings {
return Settings{}
}
func main() {
s := NewSettings()
s.Labels["env"] = "dev" // panic
}
Better:
func NewSettings() Settings {
return Settings{
Labels: map[string]string{},
}
}
5. Using positional struct literals carelessly
This works:
u := User{"Alice", 25, true}
But it is harder to read and can break when field order changes.
Prefer named fields:
u := User{Name: "Alice", Age: 25, Active: true}
Comparisons
| Approach | Best for | Pros | Cons |
|---|---|---|---|
| Zero-value struct | Simple cases where zero values are valid | Very simple, idiomatic | No custom defaults |
| Struct literal | One-off initialization | Clear and direct | Defaults may be duplicated |
| Constructor function | Reusable defaults | Centralized logic, easy to maintain | Adds an extra function |
| Constructor with parameters | Defaults plus a few overrides | Simple and practical | Can become messy with many parameters |
| Functional options | Many optional fields | Flexible, readable call sites, scalable | More code and slightly harder for beginners |
Struct literal vs constructor
| Choice | When to use |
|---|---|
| Struct literal | When the struct is small and created locally |
| Constructor | When the struct needs default values, validation, or invariants |
Value return vs pointer return
| Choice | Use when |
|---|---|
| Return value | The struct is small and copying is fine |
| Return pointer | The struct is large, mutable, or methods use pointer receivers |
Cheat Sheet
Key rules
- Go gives all struct fields a zero value automatically.
- You cannot declare custom default field values inside a struct type.
- Use a constructor function to centralize custom defaults.
- Use named field literals for readability.
- Use options when many fields are optional.
Zero values
| Type | Zero value |
|---|---|
int | 0 |
string | "" |
bool | false |
| pointer | nil |
| slice | nil |
| map | nil |
Basic constructor
func NewConfig() Config {
return Config{
Host: "localhost",
Port: 8080,
}
}
Pointer constructor
func NewConfig() *Config {
return &Config{
Host: "localhost",
Port: 8080,
}
}
Apply defaults manually
cfg := Config{}
if cfg.Host == "" {
cfg.Host = "localhost"
}
if cfg.Port == 0 {
cfg.Port = 8080
}
Watch out for
- nil maps before writing to them
- duplicated default values across files
- using
0or""to mean both "unset" and a valid value - positional struct literals that reduce readability
FAQ
Does Go support default values directly in struct definitions?
No. Go only provides zero values automatically. Custom defaults must be set during initialization.
What is the most idiomatic way to give a struct default values in Go?
Usually a constructor function such as NewConfig() is the cleanest and most maintainable approach.
Should a Go constructor return a struct or a pointer?
Either is valid. Return a value for small, simple structs. Return a pointer when the struct is large or meant to be modified.
Can I use struct tags to set default values?
Not by themselves. Struct tags are just metadata. Some libraries read tags and apply defaults, but Go does not do that automatically.
Why does Go prefer zero values?
Zero values make variables immediately usable in many cases and keep the language simple. For example, a zero-value mutex or bytes buffer can still be valid to use.
How do I handle optional fields where zero is also a valid value?
Use pointers, separate boolean flags, or functional options so you can distinguish between "not provided" and an intentional zero value.
Is it okay to create structs directly instead of using a constructor?
Yes, if zero values or explicit field values are enough. Use a constructor when you need defaults, validation, or guaranteed invariants.
Mini Project
Description
Build a small configuration system for a Go application. The project demonstrates how to create a struct with sensible defaults, allow selected overrides, and keep initialization logic in one place instead of spreading hard-coded values throughout the codebase.
Goal
Create a NewAppConfig constructor that returns a config struct with defaults and supports custom overrides for selected fields.
Requirements
- Define an
AppConfigstruct with fields for app name, port, debug mode, and labels. - Provide sensible default values for all fields.
- Allow the caller to override at least the port and debug mode.
- Ensure the
Labelsmap is initialized and safe to write to. - Print the final configuration to verify the result.
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.