Session 5 · Structs, methods and JSON#
Watch the class#
The big idea#
A struct bundles related facts into one thing with named fields: a person is a name, an age and an address, not three loose variables. Add methods and the struct can also do things. Structs also map neatly onto JSON, the format almost every web API speaks.
Everyday example: a form with labelled boxes: Name, Age, Address. Each box has a name and holds one kind of answer. Filling the form is creating a struct value.
In plain words
type Person struct { Name string; Age int } defines the shape. p := Person{Name: "Asha"} makes
one. func (p Person) Greet() attaches a method. Put a tag like `json:"name"` after a field
to control its JSON name.
Reach for it when:
several values always travel together
you are reading or writing JSON
you want behaviour next to the data it works on
Defining and using structs#
basic.go
package main
import "fmt"
type Address struct {
Street string
City string
}
type Person struct {
Name string
Age int
Active bool
Address Address // a struct inside a struct
}
// a method: a function attached to Person
func (p Person) IsEligibleToVote() bool {
return p.Age >= 18
}
func main() {
p := Person{
Name: "Jafer",
Age: 25,
Address: Address{Street: "2nd Street", City: "Coimbatore"},
}
fmt.Println(p)
fmt.Printf("%+v\n", p)
p.Age = 28
fmt.Println(p.Name, p.Age, p.Address.City, p.IsEligibleToVote())
var empty Person
fmt.Printf("%+v\n", empty)
}
{Jafer 25 false {2nd Street Coimbatore}}
{Name:Jafer Age:25 Active:false Address:{Street:2nd Street City:Coimbatore}}
Jafer 28 Coimbatore true
{Name: Age:0 Active:false Address:{Street: City:}}
Fields you do not set get their zero value (the last line).
Structs can contain other structs; reach inside with
p.Address.City.A method is a function with a receiver in front of the name:
(p Person).Fields and methods starting with a capital letter are visible to other packages; lowercase ones are private to the package.
Value or pointer receiver?#
pointer_receiver.go
package main
import "fmt"
type Counter struct{ n int }
func (c Counter) IncByValue() { c.n++ } // works on a copy
func (c *Counter) IncByPointer() { c.n++ } // works on the original
func main() {
c := Counter{}
c.IncByValue()
c.IncByValue()
fmt.Println(c.n)
c.IncByPointer()
c.IncByPointer()
fmt.Println(c.n)
}
0
2
A value receiver works on a copy, so IncByValue lost its change. A pointer receiver
(*Counter) changes the original. Rule of thumb: if any method needs to change the struct, use
pointer receivers for all of them. (Pointers get a whole session next.)
Structs and JSON#
json.go
package main
import (
"encoding/json"
"fmt"
)
type Geo struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Geo Geo `json:"geo"`
token string // lowercase: private, never in JSON
}
func main() {
data := `{"id": 1, "name": "Syed", "geo": {"lat": 12.45, "lng": 80.56}}`
var u User
if err := json.Unmarshal([]byte(data), &u); err != nil {
fmt.Println("bad json:", err)
return
}
fmt.Printf("%+v\n", u)
u.Email = "syed@example.com"
u.token = "secret"
out, _ := json.Marshal(u)
fmt.Println(string(out))
pretty, _ := json.MarshalIndent(Geo{1.5, 2.5}, "", " ")
fmt.Println(string(pretty))
err := json.Unmarshal([]byte(`{"id": "one"}`), &u)
fmt.Println(err)
}
{ID:1 Name:Syed Email: Geo:{Lat:12.45 Lng:80.56} token:}
{"id":1,"name":"Syed","email":"syed@example.com","geo":{"lat":12.45,"lng":80.56}}
{
"lat": 1.5,
"lng": 2.5
}
json: cannot unmarshal string into Go struct field User.id of type int
json.Marshalturns a struct into JSON bytes;json.Unmarshal(data, &target)fills a struct. Pass the address (&u) so Unmarshal can write into it.The
json:"name"tag sets the key.omitemptyleaves a field out when it is empty.Only exported (capitalised) fields appear in JSON, which is why
tokennever shows.Always check the error: the last line shows what a type mismatch looks like.
Common mistakes#
Lowercase field names with JSON.
name stringis invisible to thejsonpackage, so you get empty output and no error.Forgetting
&inUnmarshal. Passing the struct instead of its address changes nothing (and returns an error).Ignoring the error from
Unmarshal. Bad input silently leaves zero values.Mixing value and pointer receivers on one type; pick one style.
Comparing structs holding slices or maps with
==. It will not compile. Usereflect.DeepEqual.
Hands-on exercises#
Exercise 1 · Rectangle. Create Rectangle{W, H float64} with methods Area() and Perimeter().
Solution
package main
import "fmt"
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
func (r Rectangle) Perimeter() float64 { return 2 * (r.W + r.H) }
func main() {
r := Rectangle{W: 3, H: 4}
fmt.Println(r.Area(), r.Perimeter())
}
12 14
Exercise 2 · Bank account. Account with Balance, and a pointer-receiver Deposit(n). Show
that two deposits add up.
Solution
package main
import "fmt"
type Account struct{ Balance float64 }
func (a *Account) Deposit(n float64) { a.Balance += n }
func main() {
acc := Account{}
acc.Deposit(100)
acc.Deposit(50.5)
fmt.Println(acc.Balance)
}
150.5
Go takes the address of acc for you when you call a pointer method on a variable.
Exercise 3 · Parse a list. Decode [{"title":"Learn Go","done":true},{"title":"Relax","done":false}]
into []Task and print how many are done.
Solution
package main
import (
"encoding/json"
"fmt"
)
type Task struct {
Title string `json:"title"`
Done bool `json:"done"`
}
func main() {
data := `[{"title":"Learn Go","done":true},{"title":"Relax","done":false}]`
var tasks []Task
if err := json.Unmarshal([]byte(data), &tasks); err != nil {
fmt.Println(err)
return
}
done := 0
for _, t := range tasks {
if t.Done {
done++
}
}
fmt.Println(len(tasks), "tasks,", done, "done")
}
2 tasks, 1 done