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#

A struct and its JSON Field tags decide the JSON names. Lowercase (unexported) fields are invisible to the json package. type User struct ID int `json:"id"` Name string `json:"name"` Email string `json:"email,omitempty"` token string (private: skipped) Marshal Unmarshal { "id": 1, "name": "Syed", "email": "s@go.dev" } omitempty drops Email when empty
Field tags decide the JSON names. Lowercase (unexported) fields are invisible to the json package.

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.Marshal turns 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. omitempty leaves a field out when it is empty.

  • Only exported (capitalised) fields appear in JSON, which is why token never shows.

  • Always check the error: the last line shows what a type mismatch looks like.

Common mistakes#

  • Lowercase field names with JSON. name string is invisible to the json package, so you get empty output and no error.

  • Forgetting & in Unmarshal. 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. Use reflect.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