Now that we have covered control flow, it is time to look at how Go models data and behavior. We will do this using structs, methods, and pointers. The Task struct you define in this chapter is the exact one we will use for the rest of the course, all the way through to the database-backed API.
We are adding two new files. The first is task.go, which defines the Task struct and its methods. The second is structs.go, which holds three demo functions to exercise what we just built.

By the end of this chapter you will:
&Task{}, and new&, read through one with *, and recognize a nil pointerCheck out the finish branch:
bashgit checkout 09-structs-and-methods-finish
Struct. A named collection of fields. Go uses structs to shape data, similar to how other languages use classes, but without inheritance. You define the data shape once and attach behavior using methods.
Method. A function attached to a type. The type it attaches to is called the receiver. You write the receiver between the func keyword and the method name, like this: func (t Task) Summary() string.
Value receiver. The receiver is passed as a copy. The method can read the fields, but any changes made to the copy disappear when the method finishes.
Pointer receiver. The receiver is passed as the memory address of the original value. Changes made here affect the original.
Pointer. A variable that holds a memory address. Writing p := &x makes p point to x. Writing *p reads the value at that address. Go has pointers but no pointer arithmetic. You cannot do p++ to move to the next memory address.
Nil pointer. A pointer that points to nothing. Trying to read a nil pointer causes the program to crash at runtime.
Embedding. Placing one struct type inside another without giving it a field name. The inner type's fields and methods are promoted directly onto the outer type.
This is the struct we will carry through every remaining chapter:
go// task.gotype Task struct {ID intTitle stringDone bool}
It has three fields: an integer ID, a string title, and a boolean that tracks whether the task is finished. It is simple by design. The later API chapters will add more fields, but this core shape stays the same.
Go does not use constructor functions. You can build a Task in three different ways:
go// structs.goliteral := Task{ID: 1, Title: "write code", Done: false}pointer := &Task{ID: 2, Title: "run tests"}fresh := new(Task)fresh.ID = 3fresh.Title = "ship it"
All three approaches work. The struct literal gives you full control over the initial values. &Task{} creates a struct and immediately returns a pointer to it. This is very common in Go code. new(Task) also returns a pointer to a struct with default values. It is less common, but worth knowing.
If you leave a field out of a literal, Go automatically assigns its zero value (0, "", or false). You never have to worry about uninitialized memory.
A method is simply a function with a receiver. The Summary method returns a one-line description of a task:
go// task.gofunc (t Task) Summary() string {status := "open"if t.Done {status = "done"}return fmt.Sprintf("#%d %q [%s]", t.ID, t.Title, status)}
The (t Task) part is the receiver. When you call literal.Summary(), Go passes a copy of literal into t. The method reads the fields and returns a string. Because it never changes the data, a value receiver works perfectly here.
If you are used to Java, Python, or C#, this setup might look strange. In those languages, methods live inside the class alongside the data. Go separates them. The Task struct only lists fields. The Summary method is a separate, top-level function that happens to name Task as its receiver. Go has no classes, so methods do not belong to a class.
You might wonder why we write literal.Summary() instead of Summary(literal). Conceptually, the receiver is just a parameter moved in front of the function name. Go allows the dot notation because it reads well and groups behavior with the data. Under the hood, it is still a plain function. Thinking of the receiver as the first argument makes the rest of this chapter much easier to follow.
The difference between value and pointer receivers is a common stumbling block.
Look at these two methods side by side:
go// task.gofunc (t Task) markDoneByValue() bool {t.Done = truereturn t.Done}func (t *Task) MarkDone() {t.Done = true}
The markDoneByValue method uses a value receiver. When you call it, Go copies the Task into t. Setting t.Done = true only changes that local copy. When the method finishes, the copy disappears. The original task remains untouched.
The MarkDone method uses a pointer receiver. Go passes the memory address of the original task. Setting t.Done = true writes directly to the original struct. The change sticks.
Here is what the demo in structs.go does to make this visible:
go// structs.goinsideCopy := literal.markDoneByValue()fmt.Printf("markDoneByValue set the copy to %t, but...\n", insideCopy)fmt.Println("after markDoneByValue:", literal.Summary())literal.MarkDone()fmt.Println("after MarkDone: ", literal.Summary())
And the output:

Notice that markDoneByValue returned true. The copy did flip to done, but the original literal still prints [open]. Then MarkDone runs, and literal becomes [done]. Both methods tried to update the task, but only the pointer receiver succeeded.
If you ever call a method to update a struct and nothing changes, check your receiver. You likely modified a copy by mistake.
Use a value receiver for read-only methods. Use a pointer receiver when the method needs to modify the struct.
There is also a general rule for consistency. Once a type has a pointer receiver method, it is usually best to make all of its methods pointer receivers. Mixing them can cause issues later when you use interfaces. In real production code, you would likely make Summary a pointer receiver too. We mixed them here strictly to show how both work side by side.
The pointersDemo function shows how pointers work without using a struct at all:
go// structs.gocount := 41p := &count*p = *p + 1fmt.Printf("count is now %d (changed through the pointer)\n", count)var missing *Taskfmt.Println("a nil *Task prints as:", missing)
Writing &count takes the memory address of count and stores it in p. The variable p is now a *int, meaning a pointer to an integer. Writing *p reads the value at that address. The line *p = *p + 1 adds one to the value at the address p holds. Since this is the exact same memory as count, count becomes 42.
The second part declares missing as a *Task without assigning it a value. Its zero value is nil. Printing a nil pointer outputs <nil>. If you try to access a field on missing, like missing.ID, the program will crash. Always make sure a pointer is not nil before reading its fields.
Go has pointers, but it does not allow pointer arithmetic. You cannot write p++ to advance to the next element in memory. This makes Go pointers much safer than those in languages like C.
Go does not have class inheritance. Instead, you build complex types by composing simpler ones. Embedding is how you achieve this. You place one type inside another without giving it a field name.
go// structs.gotype labeledTask struct {TaskLabel string}
The labeledTask struct embeds Task. This means the ID, Title, Done fields, and the Summary() method are automatically promoted to labeledTask. You can use them directly:
go// structs.golt := labeledTask{Task: Task{ID: 4, Title: "review PR", Done: true},Label: "urgent",}fmt.Printf("%s (label: %s)\n", lt.Summary(), lt.Label)fmt.Println("promoted field Title:", lt.Title)
Calling lt.Summary() delegates to Task.Summary(). Reading lt.Title reaches directly into the embedded Task. You can spell it out as lt.Task.Title if you need to be explicit, but the short form is standard.
Embedding provides some of the benefits of object-oriented inheritance. However, a labeledTask is not a subclass of Task. It simply contains a Task and borrows its behavior. This distinction matters later when you use interfaces. For now, just know that embedding lets you extend a type without typing out all its fields and methods again.
Next, we will cover slices and maps. These are the two collections you will reach for constantly in Go. They are also the exact data structures that will power the in-memory task store you build a few chapters from now.