Question
In Go, error values are often checked for nil:
if err != nil {
// Handle the error.
}
However, given this struct and a Config value:
type Config struct {
host string
port float64
}
var config Config
why does this comparison produce the compiler error cannot convert nil to type Config?
if config == nil {
// ...
}
How should you determine whether a configuration value is present or has been initialized?
Short Answer
You will learn why a Go struct value cannot be nil, which Go types can be nil, and how to represent an optional Config using a pointer or an explicit validity rule.
Concept
nil means “no value” for specific Go types. It is not a universal empty-value marker.
A Config variable declared as a struct value always contains a Config value, even when you do not assign one yourself:
var config Config
This creates the struct's zero value:
Config{host: "", port: 0}
Because config is an actual struct value, it cannot be compared with nil. That is why Go reports an error.
In contrast, err is commonly an error interface value. Interfaces can be nil, so this is valid:
if err != nil {
// An error value is present.
}
If a configuration itself may be absent, represent it with a pointer:
var config *Config
if config == nil {
// No Config has been assigned.
}
This distinction matters because Go makes absence explicit. A value-type struct represents data that always exists; a pointer can represent either an address to data or no data at all.
Mental Model
Think of a struct value as a form that always exists on your desk. A new form may have blank fields, but the form is still there.
var config Config
config is that blank form. It is not nil.
A pointer is like a note containing the location of a form. The note can either contain a location or be empty:
var config *Config
When the note is empty, its value is nil. Before reading fields through the pointer, check whether the note contains a location.
Syntax and Examples
A struct value cannot be nil:
type Config struct {
host string
port float64
}
var config Config
// Invalid: Config is a struct value, not a nil-able type.
// if config == nil { }
Use a pointer when absence is meaningful:
type Config struct {
host string
port float64
}
var config *Config
if config == nil {
fmt.Println("No configuration was supplied")
}
config = &Config{
host: "localhost",
port: 8080,
}
if config != nil {
fmt.Println(config.host)
}
The &Config{...} expression creates a Config and produces a pointer to it. Since config has type *Config, comparing it with nil is valid.
If the struct must always be present, check its fields according to your application's definition of “valid”:
config := Config{host: "localhost", port: 8080}
if config.host == "" || config.port <= 0 {
fmt.Println("Configuration is incomplete")
}
There is no universal “empty struct” check. Your program must define which field values are acceptable.
Step by Step Execution
Consider this program:
package main
import "fmt"
type Config struct {
host string
port float64
}
func main() {
var config *Config
fmt.Println(config == nil)
config = &Config{host: "api.example.com", port: 443}
fmt.Println(config == nil)
fmt.Println(config.host)
}
Execution trace:
var config *Configdeclares a pointer variable.- A pointer with no assigned address gets the zero value
nil. config == nilistrue, so the first line printstrue.&Config{...}creates aConfigvalue and assigns its address toconfig.config == nilis nowfalsebecause the pointer refers to aConfig.config.hostfollows the pointer and reads thehostfield, printingapi.example.com.
By comparison, this declaration creates the value directly:
var valueConfig Config
valueConfig is immediately usable and has the zero-value fields "" and 0, but it cannot be nil.
Real World Use Cases
- Optional dependency: A service may accept an optional cache configuration. Use
*Configwhen no cache configuration is a valid state. - JSON and API requests: A pointer field can distinguish a missing nested object from a supplied object with zero-valued fields.
- Database records: A pointer is useful when a related record may not exist, such as an optional user profile.
- Configuration loading: A loader can return
*Configand an error. A non-nilerror indicates failure; a non-nilconfig indicates successful loading. - Validation: A required
Configvalue can be passed by value, then validated by checking required fields such as host and port.
Real Codebase Usage
In production Go code, choose the type based on the meaning of absence.
Required configuration: use a value
func NewServer(config Config) error {
if config.host == "" {
return fmt.Errorf("host is required")
}
if config.port <= 0 {
return fmt.Errorf("port must be positive")
}
return nil
}
This is a guard-clause style: invalid input returns early. The caller must provide a Config, and the function validates its contents.
Optional configuration: use a pointer
func NewClient(config *Config) error {
if config == nil {
return fmt.Errorf("config is required")
}
if config.host == "" {
return fmt.Errorf("host is required")
}
return nil
}
Checking config == nil must happen before accessing config.host; otherwise the program can panic by dereferencing a nil pointer.
Defaults without pointers
When zero values have a useful meaning, a value struct is often simpler:
func withDefaults(config Config) Config {
if config.port == 0 {
config.port = 8080
}
return config
}
This pattern treats port 0 as “use the default port,” rather than treating the entire config as absent.
Common Mistakes
Comparing a struct value to nil
var config Config
if config == nil { // Does not compile.
}
A struct is not a nil-able type. Use *Config if the whole object may be absent, or validate individual fields if it is always present.
Dereferencing a nil pointer
var config *Config
fmt.Println(config.host) // Panic at runtime.
Check first:
if config != nil {
fmt.Println(config.host)
}
Treating zero values as automatically invalid
if config.port == 0 {
// This is not always an error.
}
Zero is the default numeric value. It might mean “not configured,” “choose a default,” or be valid in another domain. Define and document the rule for your application.
Comparing a struct to an empty struct when fields are not comparable
For this simple struct, the following is valid because string and float64 are comparable:
if config == (Config{}) {
// All fields have their zero values.
}
However, this does not work if the struct contains a slice, map, or function field, because those fields cannot be compared except to nil:
type BadComparison struct {
tags []string
}
// BadComparison{} == BadComparison{} // Does not compile.
Prefer explicit validation methods when checking configuration quality.
Comparisons
| Type or approach | Can be nil? | Best use |
|---|---|---|
Config | No | Configuration always exists; use zero values or validation rules. |
*Config | Yes | The entire configuration may be absent. |
error | Yes | A function may have no error or an error value. |
map[string]string | Yes | An optional map; reads from a nil map are safe, writes are not. |
[]string | Yes | An optional or empty sequence; nil and empty slices often behave similarly when ranging over them. |
Config{} versus nil
Config{} // A real Config with zero-valued fields.
(*Config)(nil) // No Config is present.
Use Config{} when you need an actual value. Use nil only with a nil-able type such as *Config.
Pointer versus value parameter
func UseValue(c Config) {} // Caller always provides a Config value.
func UsePointer(c *Config) {} // Caller can pass nil.
Use a pointer when absence needs to be represented or when shared mutation is intentionally needed. Otherwise, a value is often simpler and avoids nil checks.
Cheat Sheet
// Struct value: never nil.
var c Config // Config{host: "", port: 0}
// c == nil // Invalid.
// Struct pointer: can be nil.
var p *Config // nil
if p == nil {
// No Config is present.
}
p = &Config{host: "localhost", port: 8080}
if p != nil {
fmt.Println(p.host)
}
nilis valid for pointers, interfaces, slices, maps, channels, functions, and some unsafe pointer types.- Structs are value types and cannot be
nil. - A struct's zero value is a valid struct with zero-valued fields.
- Check a pointer for
nilbefore accessing its fields. - Validate required struct fields explicitly.
config == (Config{})only compiles when every field inConfigis comparable.
FAQ
Why can err be compared to nil in Go?
error is an interface type, and interfaces can have the value nil. A non-nil error normally means an operation failed.
Can a Go struct ever be nil?
No. A struct value always exists. A pointer to a struct, such as *Config, can be nil.
How do I check whether a struct is empty in Go?
For a comparable struct, config == (Config{}) checks whether all fields have their zero values. Usually, explicit field validation is clearer and works even when fields are not comparable.
Should I use Config or *Config?
Use Config when configuration is required and has sensible zero values or validation rules. Use *Config when the entire configuration may be absent.
What happens when I access a field on a nil *Config?
The program panics at runtime because it attempts to dereference a nil pointer. Check the pointer first.
Is Config{} the same as nil?
No. Config{} is a real struct value whose fields contain their zero values. nil means no pointer, interface, map, slice, channel, or function value is present.
Why use float64 for a port?
Network ports are whole numbers, so an integer type such as int or uint16 is generally more appropriate. This does not affect whether Config can be nil.
Mini Project
Description
Build a small configuration validator for a web server. It demonstrates the difference between an absent configuration pointer and a present configuration with invalid field values.
Goal
Return a useful error when configuration is missing or incomplete, and accept a valid server configuration.
Requirements
- Define a
Configstruct withHostandPortfields. - Write a
validateConfigfunction that accepts*Config. - Return an error when the pointer is
nil. - Return an error when
Hostis empty. - Return an error when
Portis outside the valid TCP port range. - Demonstrate validation with missing, invalid, and valid configurations.
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.