Promoted Field Literals
Promoted Field Literals
Embedding already lets you read o.Number when Order embeds Table. Go 1.27 closes the other half of that story for construction: a key in a composite literal may be any valid field selector for the type, so you can write Number: 4 on the outer literal when that name promotes unambiguously. Nested Table: Table{...} is still legal and often clearer. Prefer the promoted key when the desk value is one shape and the inner type is just plumbing.
Mental model
- Embedding still creates a real field. Promotion is a selector path, not inheritance.
- Before 1.27, a composite-literal key had to be a top-level field name of that struct type. Setting an embedded field meant nesting:
Table: Table{Number: 4}. - On 1.27+, a key may be a promoted field name (
Number) or another valid field selector the type accepts. The compiler fills the same sloto.Numberwould read. - If two embeddings promote the same name, the promoted key is ambiguous. Nest (or name a field) so the path is unique.
- Reading and writing stay aligned: if
o.Streetcompiles,Street:in anOrder{...}literal is the boring twin — when unambiguous.
Worked examples
Case 1: Address fields on a ticket
Save as ticket_address.go. Ticket embeds Address. On Go 1.27 you can set Street and City as keys on the outer literal.
// ticket_address.go
package main
import "fmt"
type Address struct {
Street string
City string
}
type Ticket struct {
ID int
Address
}
func main() {
t := Ticket{
ID: 7,
Street: "12 Desk Lane",
City: "Portland",
}
fmt.Printf("ticket %d\n", t.ID)
fmt.Printf("%s, %s\n", t.Street, t.City)
fmt.Printf("via embed: %s\n", t.Address.Street)
}Run:
go run ticket_address.goOutput:
ticket 7
12 Desk Lane, Portland
via embed: 12 Desk Lane
t.Street and t.Address.Street are the same slot. The literal just stopped making you nest Address: Address{...} for the common case.
Case 2: Nested form still works
Save as ticket_address_nested.go. Same type, old construction. Use this when you want the inner type name visible, or when you are matching older code.
// ticket_address_nested.go
package main
import "fmt"
type Address struct {
Street string
City string
}
type Ticket struct {
ID int
Address
}
func main() {
t := Ticket{
ID: 7,
Address: Address{
Street: "12 Desk Lane",
City: "Portland",
},
}
fmt.Printf("%s / %s\n", t.Street, t.Address.City)
}Run:
go run ticket_address_nested.goOutput:
12 Desk Lane / Portland
Both forms build the same value. Pick the one a tired reader will parse faster.
Case 3: Equality of both constructions
Save as same_order.go. Build one Order with promoted keys and one with a nested Table literal. Compare field by field.
// same_order.go
package main
import "fmt"
type Table struct {
Number int
Seats int
}
type Order struct {
Table
Item string
Cents int
}
func main() {
promoted := Order{
Number: 4,
Seats: 2,
Item: "toast",
Cents: 350,
}
nested := Order{
Table: Table{Number: 4, Seats: 2},
Item: "toast",
Cents: 350,
}
fmt.Printf("equal? %v\n", promoted == nested)
fmt.Printf("promoted table=%d seats=%d\n", promoted.Number, promoted.Seats)
fmt.Printf("nested table=%d seats=%d\n", nested.Table.Number, nested.Table.Seats)
}Run:
go run same_order.goOutput:
equal? true
promoted table=4 seats=2
nested table=4 seats=2
Structs compare field by field. Promotion only changes how you spell the initialization.
Case 4: Ambiguous promotion — nest on purpose
Save as ambiguous_ids.go. Shift and Badge both have ID. A promoted ID: key on Assignment would be ambiguous, so the program uses nested literals (the form that always compiles).
// ambiguous_ids.go
package main
import "fmt"
type Shift struct {
ID int
Day string
}
type Badge struct {
ID int
Holder string
}
type Assignment struct {
Shift
Badge
Station string
}
func main() {
a := Assignment{
Shift: Shift{ID: 3, Day: "Monday"},
Badge: Badge{ID: 9001, Holder: "Sam"},
Station: "front desk",
}
fmt.Printf("shift=%d badge=%d station=%s\n", a.Shift.ID, a.Badge.ID, a.Station)
fmt.Printf("holder=%s day=%s\n", a.Holder, a.Day)
}Run:
go run ambiguous_ids.goOutput:
shift=3 badge=9001 station=front desk
holder=Sam day=Monday
a.Holder and a.Day promote cleanly. a.ID does not — two ID fields compete. Writing ID: 3 in the Assignment{...} literal fails for the same reason. Nest (or stop embedding one of them) when names collide.
The trap
Save as promote_not_subtype.go. Promoted keys make construction look flatter. They do not make Order a Table. Functions that want Table still need o.Table.
// promote_not_subtype.go
package main
import "fmt"
type Table struct {
Number int
Seats int
}
type Order struct {
Table
Item string
}
func freeSeats(t Table) int {
return t.Seats
}
func main() {
o := Order{
Number: 4,
Seats: 2,
Item: "tea",
}
fmt.Println(freeSeats(o.Table))
fmt.Printf("item=%s number=%d\n", o.Item, o.Number)
}Run:
go run promote_not_subtype.goOutput:
2
item=tea number=4
freeSeats(o) still does not compile. Flatter literals are sugar over the same embedding rules you already learned. If a review wants “is-a Table,” use an interface or pass o.Table — do not reach for embedding tricks.
The boring rule
- On Go 1.27+, use promoted field keys when the embedded name is unambiguous and the flatter literal reads better.
- Keep the nested
Inner: Inner{...}form when you want the inner type name on the page, when porting older snippets, or when two embeddings share a field name. - Embedding is still for “this value has that part,” not for inheritance and not merely to shorten literals.
- If
outer.Fieldis ambiguous at use sites, it is ambiguous in literals too — nest or rename. - Mutating methods still need pointer receivers; promotion does not change copying.
Try this
- In
ticket_address.go, addZip stringtoAddressand set it with a promoted key. Printt.Zipandt.Address.Zip. - In
same_order.go, change only the nested form’sSeatsto4. Confirmequal?becomesfalse. - In
ambiguous_ids.go, try addingID: 3to theAssignment{...}literal and read the compiler error. Then remove it. - Rewrite
Case 1with a named fieldAddr Addressinstead of embedding. Which literal keys still work? Why?