Introduction to Structs and Methods in Go

STOP: Each chapter of Programming for Lovers comprises two parts. First, the “core text” presents critical concepts at a high level, avoiding language-specific details. The core text is followed by “code alongs,” where you will apply what you have learned while learning the specifics of the language syntax. We strongly suggest starting with this chapter’s core text (click the button below) to grasp the fundamentals before moving onto code alongs to see the application of these fundamentals. Once you have done so, you are ready to proceed with this code along!

Learning objectives

In the core text, we showed a language-neutral way of representing Rectangle and Circle objects as a collection of fields. In this code along, we will explore how Go implements custom data types using structs and methods, using the same shapes example. Along the way we will introduce one of Go’s most important concepts: pointers. Understanding pointers here will prepare us to implement a fully functional gravity simulator in the next two code alongs.

Setup

Create a folder called shapes in your go/src/ directory. Create a text file called main.go in the go/src/shapes folder. We will edit main.go throughout this code along. It should start with the following starter code.

package main

import "fmt"

func main() {
	fmt.Println("Shapes.")
}

Defining structs

Recall the language-neutral representations of Rectangle and Circle from the core text.

type Rectangle
    x1 float
    y1 float
    width float
    height float
    rotation float

type Circle
    x1 float
    y1 float
    radius float

In Go, we define a custom data type using the keyword type, followed by the name of the type, followed by the keyword struct, followed by a list of field names and their types enclosed in curly braces. We declare Rectangle and Circle structs as follows.

type Rectangle struct {
	width, height float64
	x1, y1        float64
	rotation      float64
}

type Circle struct {
	x1, y1 float64
	radius  float64
}

Add these two struct declarations to main.go, placing them above the main() function.

Note: Good Go practice is to place type declarations at the top of the file, after the import block and before any function definitions.

Zero values in Go

Unlike Python, Go does not require a constructor function to set default values. When we declare a struct variable in Go, all of its fields are automatically initialized to their zero values: 0.0 for float64, 0 for integers, false for booleans, and "" for strings. We can declare a zero-valued Rectangle and Circle variable as follows.

func main() {
	fmt.Println("Shapes.")

	var r Rectangle
	var myCirc Circle
}

Now that we have declared these two variables, we can update their fields however we like by using dot notation. Let’s set some fields of myCirc and r.

func main() {
	fmt.Println("Shapes.")

	var r Rectangle
	var myCirc Circle

	myCirc.x1, myCirc.y1 = 1.0, 3.0
	myCirc.radius = 2.0
	r.width = 3.0
	r.height = 5.0
}

We can also initialize a struct directly using a struct literal, which is a compact way to declare a variable and set its fields at the same time. We list the fields by name, separated by commas. Any field we omit is set to its zero value.

r2 := Rectangle{width: 3.0, height: 5.0}   // x1, y1, rotation default to 0.0
c2 := Circle{x1: 1.0, y1: 3.0, radius: 2.0}

Printing a struct

Let’s update main() to print our two objects and verify their fields.

func main() {
	fmt.Println("Shapes.")

	var r Rectangle
	var myCirc Circle

	myCirc.x1, myCirc.y1 = 1.0, 3.0
	myCirc.radius = 2.0
	r.width = 3.0
	r.height = 5.0

	fmt.Println(r)
	fmt.Println(myCirc)
}
STOP: In a new terminal window, navigate into our directory using cd go/src/shapes. Then run your code by executing go run main.go. What do you see?

When we run the code, fmt.Println prints a struct by showing all of its field values enclosed in curly braces. On our computer we obtain:

Shapes.
{3 5 0 0 0}
{1 3 2}

The values are shown in the order the fields appear in the struct declaration. This is a convenient built-in behavior — no extra work is needed to get a readable printout of our struct’s fields.

Methods in Go

In Go, a method is a function that is associated with a specific type. The method’s connection to the type is established by a receiver argument, which appears between the func keyword and the method’s name. For example, we can define an Area() method for both Circle and Rectangle as follows.

func (c Circle) Area() float64 {
	return 3.14159 * c.radius * c.radius
}

func (r Rectangle) Area() float64 {
	return r.width * r.height
}

Here, c Circle and r Rectangle are the receivers. To call these methods, we use the same dot notation we use to access fields.

fmt.Println("myCirc's Area is", myCirc.Area())
fmt.Println("r's Area is", r.Area())

Notice that unlike Python, Go does not require us to use different function names for Area() on a Circle versus a Rectangle. Go resolves which method to call based on the receiver type.

Pointer receivers

The Area() methods above are value receivers: they receive a copy of the struct. This means that if a method tries to modify the struct’s fields, those modifications will not persist after the method returns. To write a method that modifies the struct in place, we need a pointer receiver.

Before we can explain pointer receivers, we need to understand what a pointer is. A pointer is a variable that stores the memory address of another variable rather than the variable’s value directly. In Go, we declare a pointer to a type using a * in front of the type name. For example, var a *int declares a variable a that holds the address of some integer. Initially, a holds the special value nil, meaning it points to nothing.

var b int = -14
var a *int  // a has type "pointer to int"; starts as nil

a = &b      // & gives us the address of b; now a points to b
fmt.Println("b is", b)

*a = 4      // * dereferences the pointer: go to the address a holds and change the value there
fmt.Println("b is now", b) // b is now 4, because a pointed to b

The & operator gives us the address of a variable, and the * operator (when placed before a pointer variable) dereferences it, giving us the value at that address. Running this code prints:

b is -14
b is now 4

Pointers work with structs as well. We can declare a pointer to a Circle, point it at myCirc, and then modify myCirc’s fields through the pointer. Go gives us a convenient shorthand: instead of writing (*pointerToCirc).x1 to access a field through a pointer, we can write pointerToCirc.x1 directly.

var pointerToCirc *Circle
pointerToCirc = &myCirc

pointerToCirc.x1 = 0.0  // modifies myCirc directly
pointerToCirc.y1 = 0.0

fmt.Println("circle center:", myCirc.x1, myCirc.y1) // 0 0

Now we can understand pointer receivers. A pointer receiver is a receiver whose type is *Rectangle or *Circle rather than Rectangle or Circle. A method with a pointer receiver can modify the struct it is called on. Let’s define Translate() methods for both Rectangle and Circle using pointer receivers so that they modify the struct in place.

func (r *Rectangle) Translate(a, b float64) {
	r.x1 += a
	r.y1 += b
}

func (c *Circle) Translate(a, b float64) {
	c.x1 += a
	c.y1 += b
	fmt.Println("c's center is at", c.x1, c.y1)
}

Even though myCirc is not a pointer, Go is smart enough to automatically take its address when we call a pointer receiver method on it. So we can call myCirc.Translate(1.3, 4.5) directly, and Go will treat this as (&myCirc).Translate(1.3, 4.5) behind the scenes.

myCirc.Translate(1.3, 4.5)
fmt.Println("circle center:", myCirc.x1, myCirc.y1)

The Scale method

Let’s define one more method: Scale(), which multiplies the dimensions of a shape by a factor f. Since Scale() must modify the struct’s fields, we again use pointer receivers.

func (r *Rectangle) Scale(f float64) {
	r.width *= f
	r.height *= f
}

func (c *Circle) Scale(f float64) {
	c.radius *= f
}

Let’s test Scale() on myCirc in main().

myCirc.radius = 3.0
myCirc.Scale(2.0)
fmt.Println("new radius:", myCirc.radius) // 6

Creating a pointer with new

Go provides a built-in function called new() that allocates a zero-valued instance of a type and returns a pointer to it. Using new(Circle), for example, creates a new Circle behind the scenes and returns a *Circle that points to it.

d := new(Circle) // d is a *Circle pointing to a zero-valued Circle
d.x1 = 3.0
d.y1 = -2.45
d.radius = 5.0
fmt.Println("Area of d is", d.Area())
fmt.Println(d)

Notice that we can call d.Area() even though d is a pointer rather than a Circle value. Go automatically dereferences pointer receivers when calling methods. Printing d will print its address, while printing *d will print its fields.

STOP: Assemble the full main() function using all the pieces we have built so far, and run the program. Verify that printing *d shows {3 -2.45 5}.

With structs and methods in hand, we are now ready to move on to implementing the gravity simulator. In the next code along, we will define the data types for our celestial bodies and implement the file reading and drawing components of the simulator.

Scroll to Top
Programming for Lovers banner no background
programming for lovers logo cropped

Join our community!

programming for lovers logo cropped
Programming for Lovers banner no background

Join our community!