Structs and Data Modeling
Structs and Data Modeling
A struct is a named bag of fields. The boring default is a few structs for the desk (ticket, order, shift), composite literals that name their fields, and embedding only when promotion is the point — not as a substitute for inheritance, which Go does not have.
Mental model
type Ticket struct { ... } defines a type. Values are copied on assignment. A field name that starts with an upper-case letter is exported (visible from another package). Lower-case is package-private.
A composite literal Ticket{ID: 7, Table: 12} sets the named fields; the rest are zero. Embedding writes another type as a field without a name. Methods and fields of the inner type are promoted to the outer type. The outer type is still not “a kind of” the inner type.
A tag is a string on a field (json:"id"). Encoding packages read tags. Your code usually ignores them.
Worked examples
Case 1: Ticket and order literals
Save as desk_structs.go. Named fields, then a print.
// desk_structs.go
package main
import "fmt"
type Ticket struct {
ID int
Table int
}
type Order struct {
Ticket Ticket
Item string
Cents int
}
func main() {
t := Ticket{ID: 7, Table: 12}
o := Order{Ticket: t, Item: "toast", Cents: 350}
fmt.Printf("ticket %d table %d\n", o.Ticket.ID, o.Ticket.Table)
fmt.Printf("%s %d cents\n", o.Item, o.Cents)
}Run:
go run desk_structs.goOutput:
ticket 7 table 12
toast 350 cents
Order{Ticket: t, ...} is clearer than positional Order{t, "toast", 350}. Use names.
Case 2: Exported fields and a JSON tag
Save as ticket_json.go. note is lower-case, so encoding/json skips it. The tags rename the exported fields in the JSON.
// ticket_json.go
package main
import (
"encoding/json"
"fmt"
)
type Ticket struct {
ID int `json:"id"`
Table int `json:"table"`
note string `json:"note"`
}
func main() {
t := Ticket{ID: 7, Table: 12, note: "window"}
b, err := json.Marshal(t)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(b))
}Run:
go run ticket_json.goOutput:
{"id":7,"table":12}
note is in the struct and gone from the JSON. Other packages cannot write t.note either. Export the field (Note) when callers and encoders should see it.
Case 3: Embedding promotes fields and methods
Save as embed_table.go. Order embeds Table. o.Number is o.Table.Number. o.Label() is o.Table.Label().
// embed_table.go
package main
import "fmt"
type Table struct {
Number int
Seats int
}
func (t Table) Label() string {
return fmt.Sprintf("table %d", t.Number)
}
type Order struct {
Table
Item string
Cents int
}
func main() {
o := Order{
Table: Table{Number: 4, Seats: 2},
Item: "toast",
Cents: 350,
}
fmt.Println(o.Number, o.Seats, o.Label())
fmt.Println(o.Item, o.Cents)
}Run:
go run embed_table.goOutput:
4 2 table 4
toast 350
Embedding is a shorter path to the inner fields. It is still a field. o.Table exists.
Case 4: Embedding is not inheritance
Save as not_a_table.go. A function that wants a Table will not take an Order. You pass o.Table.
// not_a_table.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{Table: Table{Number: 4, Seats: 2}, Item: "tea"}
fmt.Println(freeSeats(o.Table))
}Run:
go run not_a_table.goOutput:
2
freeSeats(o) does not compile: cannot use o (variable of struct type Order) as Table value. There is no subclass. There is a field.
The trap
A struct value is a copy. A method or function that takes Ticket (not *Ticket) cannot update the caller’s fields. Save as copy_update.go:
// copy_update.go
package main
import "fmt"
type Ticket struct {
ID int
Table int
}
func (t Ticket) move(table int) {
t.Table = table
}
func (t *Ticket) movePtr(table int) {
t.Table = table
}
func main() {
t := Ticket{ID: 7, Table: 12}
t.move(4)
fmt.Println("after value", t.Table)
t.movePtr(4)
fmt.Println("after pointer", t.Table)
}Run:
go run copy_update.goOutput:
after value 12
after pointer 4
move changed a copy. The desk still thinks table 12. Use a pointer receiver when the method’s job is to change the struct. Use a value receiver when the method only reads (see methods later). Until then: if you mutate, pass *Ticket.
The boring rule
- Name fields in literals. Leave unused fields zero.
- Export fields that other packages (and JSON) must see. Keep the rest lower-case.
- Tags are for encoders. Add them when you encode, not “just in case.”
- Embed when the inner type is a part of the outer value and promotion helps. Otherwise use a named field (
Ticket Ticket). - Do not pretend embedding is inheritance. Pass
o.Table, noto, into functions that wantTable. - Mutating methods take a pointer. Assignment copies the struct.
Try this
- In
desk_structs.go, add aShiftstruct (Name string,Tables int) and print one literal. - In
ticket_json.go, renamenotetoNoteand run. The JSON should include"Note":"window"unless you add a tag. - In
embed_table.go, printo.Table.Numbernext too.Number. They are the same slot. - In
not_a_table.go, uncomment a callfreeSeats(o)in your head, then try it and read the compile error.