1. Struct literals in Go: what they are and why they matter
When you’re just starting to program, you really want to do this: declare a variable — assign it — then assign it again — and it kind of works. But as soon as your data has more than two fields, the code starts to look like assembling a wardrobe without instructions: you have the parts, you have the screws, and at the end you still have one extra shelf left over (and you’re afraid to ask where it goes).
A struct literal fixes that pain: it lets you create one value of type Task (or any other struct) all at once, in one place. This improves readability, reduces the chance of forgetting a field, and helps you quickly “see the data model” right in the code.
The T{...} form in Go
A struct literal is an expression of the form T{...}, where T is the name of the struct type. Inside the braces you list field values. In Go this is called a composite literal, but we won’t obsess over the term: the key point is that you create a value right here and now.
Let’s start with the simplest possible example: a point struct and creating one.
package main
import "fmt"
type Point struct {
X int
Y int
}
func main() {
p := Point{X: 3, Y: 7}
fmt.Println(p.X, p.Y) // 3 7
}
Notice two things. First, p := Point{...} creates a value of type Point. Second, we used named initialization (X: 3, Y: 7). This is usually the safest style, but we’ll get to it deliberately.
2. Positional initialization: fast, but tricky
Positional initialization looks very tempting: write the values in order — done. Especially if the fields are simple and the struct is small. But there’s a catch: a positional literal is tightly coupled to the field order in the struct declaration. That is, Go matches the first value to the first field, the second value to the second field, and so on.
Let’s introduce our “through-line” model for the day: a task in a mini todo app. For now, no files, no database, and no rocket science — just an in-memory list of tasks.
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func main() {
t := Task{1, "Buy milk", false}
fmt.Println(t.ID, t.Title, t.Done) // 1 Buy milk false
}
It works. But now imagine that a week later you decide to change the order of fields in the struct (for example, you want Title to be first because “it looks nicer”). With a positional literal, the compiler won’t always save you — it will just keep matching by position. And sometimes you’ll get a silent logical bug.
The worst case is when field types are the same. Then the compiler cannot tell at all that you swapped the meaning.
package main
import "fmt"
type User struct {
FirstName string
LastName string
}
func main() {
u := User{"Ivan", "Petrov"}
fmt.Println(u.FirstName, u.LastName) // Ivan Petrov
// Now imagine you accidentally swapped them:
u2 := User{"Petrov", "Ivan"}
fmt.Println(u2.FirstName, u2.LastName) // Petrov Ivan (the compiler is fine with it)
}
That’s why positional literals love tiny examples, but don’t handle real life well. Real life doesn’t handle much either, but positional literals especially.
To lock in the difference “in your head,” a short diagram helps:
flowchart LR
A["Task{ 1, 'Buy milk', false }"] --> B["ID = 1"]
A --> C["Title = 'Buy milk'"]
A --> D["Done = false"]
Everything depends on the order. Change the order of fields in type Task struct { ... } — and you change the meaning of the literal, sometimes even without compilation errors.
3. Named initialization: readable and robust
Named initialization is when you explicitly write which field is filled with what: Task{ID: 1, Title: "Buy milk", Done: false}. At first it may feel “too verbose,” but in real code it’s like labels on moving boxes: yes, it’s annoying to write, but later you won’t be looking for the kettle in a box labeled “misc #7.”
Let’s rewrite the task in the “right” style:
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func main() {
t := Task{ID: 1, Title: "Buy milk", Done: false}
fmt.Println(t.ID, t.Title, t.Done) // 1 Buy milk false
}
The advantage of named fields is that order no longer matters:
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func main() {
t := Task{Done: false, Title: "Buy milk", ID: 1}
fmt.Println(t.ID, t.Title, t.Done) // 1 Buy milk false
}
In this style, the code is much easier to read at a glance. By the way, in official Go materials you’ll constantly see this approach too: for example, in the gob example you’ll find a literal like T{X: 7, Y: 0, Z: 8}, where fields are specified by name.
Partial initialization and zero value
Another important feature of a named literal: you can omit some fields. Unspecified fields become the zero value.
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func main() {
t := Task{Title: "Read Go spec"}
fmt.Printf("%d %q %v\n", t.ID, t.Title, t.Done) // 0 "Read Go spec" false
}
This is very convenient, but it’s important not to confuse “technically created” with “logically correct.” Today we won’t build full model validation, but keep this in mind: if ID must be positive, then Task{Title: ...} can be a convenient intermediate value, but not necessarily a “valid task.”
4. You cannot mix styles
This section usually starts with a hopeful idea: “Can I write half by position and half by name so it’s both fast and pretty?” In Go the answer is strict, like a compiler in a bad mood: no, you can’t. A struct literal is either positional or named.
Let’s show correct variants side by side so you get used to recognizing the style visually.
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func main() {
a := Task{1, "Write code", false} // positional
b := Task{ID: 2, Title: "Test code", Done: true} // named
fmt.Println(a.Title) // Write code
fmt.Println(b.Title) // Test code
}
And this is not allowed (the compiler will reject it):
// Task{1, Title: "oops", false} // not allowed: mixing styles
Why is this restriction useful? Because mixing quickly turns into “everyone does their own thing”: it’s hard to read, and even harder to maintain. Go generally prefers unambiguous style.
5. A pointer to a literal: &T{...}
Now let’s add a small but very practical detail. Sometimes you don’t just need to create a struct — you need to immediately get a pointer to it, i.e., *Task. For that you write &Task{...}.
Why? In everyday terms: to pass “the same task” to different functions and change its fields without copying the value. In Go this is a normal technique; the key is to understand that you’re getting an address.
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func main() {
p := &Task{ID: 1, Title: "Water the plants"}
p.Done = true
fmt.Println(p.Title, p.Done) // Water the plants true
}
Notice: we access pointer fields the same way, with a dot (p.Done = true). Go will automatically dereference the pointer “under the hood” where possible, so you don’t have to write (*p).Done. This is done on purpose so the code doesn’t look like a hedgehog made of parentheses.
6. Struct literals inside slices
If struct literals are useful on their own, then combined with slices they become a real “turbo mode.” A very common real-world scenario is generating demo data, or building an in-memory list of entities.
Let’s make a function that returns an initial list of tasks for our mini app. Note: the code is small, but this is already the beginning of a real application — we have the Task model and we create multiple values.
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func demoTasks() []Task {
return []Task{
{ID: 1, Title: "Buy milk", Done: false},
{ID: 2, Title: "Read docs", Done: true},
{ID: 3, Title: "Write some Go", Done: false},
}
}
func main() {
tasks := demoTasks()
fmt.Println(len(tasks)) // 3
}
Two “small details” here can save you hours later. First, we use named fields inside the slice: even in a big list, you won’t mix up where ID is and where Done is. Second, every line ends with a comma — this is especially important when the literal spans multiple lines.
7. Multi-line literals and the comma
Now for the classic moment of “why is the compiler so strict?” Go has a rule: in multi-line literals, a comma after each element is mandatory. This applies to both []T{...} and T{...} when written across multiple lines.
First, here’s the correct version. It may look “a bit noisy,” but it’s easy to add new elements: just insert a line — and you’re done.
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func main() {
t := Task{
ID: 10,
Title: "Multiline literal",
Done: false,
}
fmt.Println(t.ID, t.Title, t.Done) // 10 Multiline literal false
}
The comma after false, is not decorative. It’s needed so Go can unambiguously understand where an element ends, especially if formatting changes (and it will change — if only because you’ll hit “auto-format” in your IDE).
8. Comparing the two styles
To reinforce the decision of “what to use by default,” it helps to have a simple comparison in front of you. Not as a law of nature, but as a practical rule for a beginner.
| Criterion | Positional T{v1, v2} | Named T{Field: v} |
|---|---|---|
| Readability | quickly becomes “what’s going on here?” | almost always clear without extra context |
| Refactoring resilience | breaks when field order changes | field order in a struct is usually not important |
| Risk of silent bugs | high (especially if types are the same) | low (hard to “accidentally swap”) |
| Partial initialization | no (you must specify everything) | yes (the rest is zero value) |
| Typical style in real Go code | rare, very localized | most often, exactly this |
A practical recommendation is simple: for data models, almost always choose named fields. Leave the positional form for very small structs that are local, obvious, and unlikely to change.
9. Common mistakes when working with struct literals
At this point it often turns out the problem isn’t struct — it’s that we’re human: we get tired, we rush, we go “good enough.” So let’s go through the typical pitfalls in advance, so at least you’ll step on them consciously.
Mistake #1: using positional initialization for a “model” you’ll be changing.
A positional literal is great until the day you add a field in the middle of the struct or reorder fields. The code may still compile, but the meaning will change. The fix is simple: for Task, User, Order, and any “domain entities,” choose Field: value.
Mistake #2: mixing up values in a positional literal when field types are the same.
This is the trickiest case because the compiler won’t complain: User{"Petrov", "Ivan"} looks just as valid as User{"Ivan", "Petrov"}. If a struct has two or three fields of the same type (string, int), positional style turns into a lottery. Named fields remove the problem completely.
Mistake #3: expecting that “unspecified fields will somehow fill themselves in.”
In Go, that doesn’t happen. An unspecified field in a named literal gets the zero value (0, "", false, nil). That’s normal and often convenient, but if a field is “required by meaning,” you must set it explicitly (and validate correctness with separate code).
Mistake #4: forgetting the comma in a multi-line literal.
A classic: you nicely spread Task{ ... } across multiple lines, then add a new line, forget the comma — and the compiler complains as if you were trying to summon a demon. The fix is simple: put commas at the end of lines in multi-line literals and don’t hesitate to use auto-formatting.
Mistake #5: thinking that &Task{...} is “the same struct, just with an ampersand.”
&Task{...} returns type *Task, i.e., a pointer. It’s not a “tiny symbol,” it’s a different type. A good habit: if you’re unsure, temporarily print the type via fmt.Printf("%T\n", x) and make sure you have Task or *Task — whichever you actually intended.
GO TO FULL VERSION