Skip to content

05 - Making Your Own Types

What this session is

About an hour. You'll learn how to define your own types by grouping related data together (a struct), and how to attach methods to those types (functions that "belong to" a type). This is where Go starts to feel less like a calculator and more like a tool for modeling real things.

The problem this solves

So far, every piece of data you've worked with has been a single value - one int, one string, one bool. Real things have many properties at once. A person has a name, an age, a city. A point on a graph has an x and a y. A rectangle has a width and a height.

You could pass each property as a separate parameter:

func describe(name string, age int, city string) {
    fmt.Printf("%s, age %d, lives in %s\n", name, age, city)
}

That works for two or three. At six, you're sad. At twelve, you're lost. A struct lets you bundle them up.

A struct

package main

import "fmt"

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    alice := Person{Name: "Alice", Age: 30, City: "Lagos"}
    fmt.Println(alice)
    fmt.Println(alice.Name)
    fmt.Println(alice.Age)
}

Type it. Run it. You'll see:

{Alice 30 Lagos}
Alice
30

What's new:

  • type Person struct { ... } - defines a new type called Person. It's a struct (a bundle of fields). The fields are listed inside the braces, one per line, each with a name and a type.
  • Person{Name: "Alice", Age: 30, City: "Lagos"} - creates a value of type Person, with the listed fields set. This is called a struct literal.
  • alice.Name - dot notation to read a field. Works the same way to write: alice.Name = "Alicia" changes the name.

Naming convention: types are CapitalCase. Field names are also CapitalCase if you want them visible outside the package (more on that in page 11). For now, capitalize everything.

Structs as function parameters and returns

A struct value is just like any other value - you can pass it to functions and return it from them:

package main

import "fmt"

type Point struct {
    X, Y int
}

func origin() Point {
    return Point{X: 0, Y: 0}
}

func describe(p Point) {
    fmt.Printf("at (%d, %d)\n", p.X, p.Y)
}

func main() {
    o := origin()
    describe(o)
    describe(Point{X: 3, Y: 4})
}

Notice X, Y int - when adjacent fields share a type, you can group them on one line. Same idea as parameters.

Methods: functions attached to a type

A method is a function that belongs to a type. The only difference from a regular function is the receiver - an extra parameter declared before the function name.

package main

import (
    "fmt"
    "math"
)

type Point struct {
    X, Y float64
}

func (p Point) DistanceFromOrigin() float64 {
    return math.Sqrt(p.X*p.X + p.Y*p.Y)
}

func main() {
    p := Point{X: 3, Y: 4}
    fmt.Println(p.DistanceFromOrigin())   // 5
}

The line:

func (p Point) DistanceFromOrigin() float64 {

reads as: "define a method called DistanceFromOrigin. It's attached to the Point type. Inside the method, the value the method was called on is named p."

So p.DistanceFromOrigin() calls the method with p as the receiver.

New thing in this example: float64 is a type for numbers with decimal places. math.Sqrt takes a float64. We changed Point.X and Point.Y from int to float64 so it works.

You could have written this as a regular function:

func distanceFromOrigin(p Point) float64 { ... }

That's not wrong. But methods read better at the call site:

p.DistanceFromOrigin()        // method
distanceFromOrigin(p)         // regular function

The method form puts the most important thing (the point) first. As programs get bigger, this matters.

Pointer receivers (preview)

There's a second form of receiver, with a * in front of the type:

func (p *Point) MoveBy(dx, dy float64) {
    p.X += dx
    p.Y += dy
}

This is called a pointer receiver. The short version: use it when the method needs to modify the value. The version with just Point (no *) is called a value receiver; the method works on a copy and changes don't stick.

You'll learn pointers properly in Page 08 - don't worry about the details now. Just notice the syntax. The rule of thumb for now: if the method changes a field, use *Type as the receiver.

Constructors (a convention, not a feature)

Go has no special "constructor" keyword. The convention is to write a regular function that returns a value of your type, named NewYourType:

func NewPerson(name string, age int) Person {
    return Person{Name: name, Age: age, City: "unknown"}
}

// Use it:
alice := NewPerson("Alice", 30)

This is just a function. The pattern gives you somewhere to put validation, defaults, or setup logic. Look for NewX functions in any Go library - they're everywhere.

Exercise

In a new file shapes.go:

  1. Define a struct Rectangle with two float64 fields: Width and Height.

  2. Add two methods on Rectangle:

  3. Area() float64 - returns Width * Height.
  4. Perimeter() float64 - returns 2 * (Width + Height).

  5. In main, create a Rectangle with width 5 and height 3. Print its area and perimeter. (Expected: 15 and 16.)

  6. Write a regular function LargerOf(a, b Rectangle) Rectangle that returns whichever rectangle has the bigger area. Test it with two rectangles of different sizes.

  7. Add a constructor NewSquare(side float64) Rectangle that returns a Rectangle with both fields set to side. Test it: NewSquare(4).Area() should be 16.

What you might wonder

"Why uppercase field names?" In Go, capitalizing a name makes it visible from outside the package (other people's code can see it). Lowercase keeps it private to the current package. You'll meet packages properly in page 11. For now: capital is the safer default.

"What's the difference between a struct and a class?" If you've used a language with classes (Java, Python, C++): a Go struct is similar but smaller. No inheritance. No constructors built in. Methods are attached separately (not inside the type declaration). Go gets away with this minimalism because it has interfaces (page 11) for the cases other languages use inheritance for.

"Can I have a struct that contains another struct?" Yes. Very common. A Person could contain an Address field of type Address. Field access chains: p.Address.Street.

"What's the zero value of a struct?" If you write var alice Person without initializing fields, every field gets its zero value - 0 for numbers, "" for strings, false for booleans, nil for the things we haven't met yet. Useful: a fresh Person is always valid-looking, not garbage.

Done

You can now: - Define your own types with type Name struct { ... }. - Create struct values with the Type{Field: value, ...} literal. - Read and write fields with dot notation. - Pass structs to functions and return them. - Attach methods to types with a receiver. - Recognize the pointer-receiver syntax (*Type) without knowing pointers fully yet.

You can now model things that have multiple properties. Combined with what came before, you can write programs that work with people, products, accounts, points on graphs - anything with structure.

Next page: how Go handles collections - lists of things, lookups by key. Slices and maps.

Next: Many things at once06-slices-and-maps.md

Comments