10.2 Struct Literals: Positional and Named Field Forms
Right, let’s talk about giving your structs actual life. You’ve defined a beautiful blueprint with type MyStruct struct {...}, but a blueprint isn’t a house. To get an actual instance—a real, living, breathing chunk of data in memory—you need a struct literal. And Go, in its frustratingly pragmatic way, gives you two main flavors to choose from: positional and named. One is terse and dangerous, the other is verbose and safe. You can probably guess which one I use 99% of the time.
The Perils of Positional Initialization
The positional form is the old-school, C-like way. You just provide the values for the fields in the exact order they were declared in the struct definition. It looks clean and simple, and it is—until it isn’t.
type ServerConfig struct {
Protocol string
Host string
Port int
Timeout time.Duration
}
// Positional literal: values in declaration order
config := ServerConfig{"tcp", "localhost", 8080, 30 * time.Second}
fmt.Printf("Server: %s://%s:%d (Timeout: %v)\n", config.Protocol, config.Host, config.Port, config.Timeout)
// Output: Server: tcp://localhost:8080 (Timeout: 30s)
So why is this a “peril”? Two words: brittle refactoring. Let’s say you’re a genius and realize every server needs a MaxConnections field. You add it to the struct. But if you add it to the middle of the struct definition, every single existing positional initialization for ServerConfig suddenly, and silently, becomes completely wrong.
type ServerConfig struct {
Protocol string
Host string
MaxConnections int // <- New field!
Port int
Timeout time.Duration
}
// This existing code now compiles but is catastrophically broken.
// "localhost" is now assigned to MaxConnections?!
config := ServerConfig{"tcp", "localhost", 8080, 30 * time.Second}
The compiler happily lets you assign a string to an int here because you’re still providing the right number of arguments. It’s a silent landmine waiting for you or some poor soul on your team to trip over during a refactor. For this reason alone, I consider the positional form essentially legacy code. Use it only for tiny, throwaway, or famously stable structs (looking at you, image.Point).
The Clarity of Named Fields
This is the way. The named field syntax is verbose, yes, but it’s explicit, self-documenting, and, most importantly, immune to field ordering.
// Named field literal - order doesn't matter
config := ServerConfig{
Protocol: "https",
Port: 443,
Host: "api.example.com",
Timeout: 15 * time.Second,
// MaxConnections: 100, // We can omit it; it will get the zero value.
}
fmt.Println(config.Host) // api.example.com
See what we did there? We listed the fields in a different order than they were declared. The compiler matches everything by name, so it doesn’t care. This is a huge win for maintainability. You can add new fields to the struct without breaking every initialization site. You can also omit any fields you want—they’ll be set to their sensible zero values ("" for string, 0 for int, nil for pointers/slices/maps, etc.).
The Zero-Value Omission “Trick”
This leads to a powerful, idiomatic pattern. Design your structs so that the zero values are sensible defaults. Then, when initializing, you only need to specify the fields that deviate from the default. A user of your struct can create a useful instance with very little code.
// Let's assume a sensible default for Timeout is 30s and Protocol is "tcp"
func NewDefaultConfig(host string, port int) ServerConfig {
return ServerConfig{
Host: host, // Only set what we need to
Port: port,
// Protocol and Timeout get their zero-value defaults.
}
}
The One Edge Case: Mixing Forms (Don’t Do It)
You can, technically, mix both forms. But for the love of all that is holy, don’t. The rules are bizarre: once you use a named field, all subsequent fields must be named. And any fields you don’t name must be in the correct order. It’s the worst of both worlds—the fragility of positional and the verbosity of named.
// This compiles but is a code smell. Just don't.
weirdConfig := ServerConfig{
"tcp", // Positional for first field (Protocol)
Host: "localhost", // Now we're named. Everything after this must be named.
Port: 8080,
// You CANNOT positionally set Timeout here.
}
Stick to one style. And unless you have a spectacularly good reason, make that style the named field form. Your future self, and anyone else who has to read your code, will thank you for the clarity.