Tarkistetaan oppitunnin käyttöoikeutta...

Modeling Data

Model a Task with Structs

Video ja luettava22 minEsikatselu

Katselet ilmaista esikatseluoppituntia.

Goal

By the end of this lesson, Tasker has a real Task type. It holds all the information about one task in a single value, and the CLI uses it.

What you will learn

  • How to define your own type with struct
  • How to create struct values and read and change their fields
  • The zero value of a struct
  • How to write a constructor function such as New
  • Why Go copies a struct when you assign it or pass it to a function
  • How to print structs for debugging
  • How to work with dates and times using the time package

Why this matters

Right now, Format takes three separate values: id, title, and done. Every new piece of information about a task means one more parameter, and every caller must pass the values in the right order. This does not scale. A task is one thing, so it should be one value.

Step 1: Define the Task type

Replace task/task.go with this:

package task

import (
	"fmt"
	"time"
)

// MaxTitleLength is the longest title a task can have.
const MaxTitleLength = 50

// Task is one item on the to-do list.
type Task struct {
	ID        int
	Title     string
	Done      bool
	CreatedAt time.Time
}

// New creates an open task with the given ID and title.
func New(id int, title string) Task {
	return Task{
		ID:        id,
		Title:     title,
		CreatedAt: time.Now(),
	}
}

// Format returns one line that describes a task.
func Format(t Task) string {
	status := "[ ]"
	if t.Done {
		status = "[x]"
	}
	return fmt.Sprintf("%d. %s %s", t.ID, status, t.Title)
}

type Task struct { ... } creates a new type named Task. A struct is a group of named values called fields. Each field has a name and a type.

The field names start with capital letters, so they are exported. Code in other packages, such as main, can read and change them. A field that starts with a small letter is visible only inside the task package. The rule from Lesson 1.2 applies to fields too.

time.Time is a struct from the standard library. It stores a date and a time. Structs can hold other structs.

Step 2: Experiment with struct values

Before you change the CLI, try structs in a small test program. Create a folder named play inside tasker, and add play/main.go:

package main

import (
	"fmt"

	"tasker/task"
)

func main() {
	var empty task.Task
	fmt.Printf("%+v\n", empty)

	t := task.Task{ID: 1, Title: "Buy milk"}
	fmt.Println(t.Title)

	t.Title = "Buy oat milk"
	fmt.Println(t.Title)
}

Run only this folder:

go run ./play

Output:

{ID:0 Title: Done:false CreatedAt:0001-01-01 00:00:00 +0000 UTC}
Buy milk
Buy oat milk

Three things happen here.

The zero value of a struct. var empty task.Task creates a task with no values set. Each field gets its own zero value: 0, "", false, and the zero time (the year 1). A struct is always ready to use, like every other Go type.

A struct literal. task.Task{ID: 1, Title: "Buy milk"} creates a task and sets two fields by name. The fields you leave out get their zero values, so Done is false.

Field access. You read and change fields with a dot: t.Title.

The %+v verb prints a struct with its field names. %v prints only the values. Both are very useful for debugging.

Always use field names in struct literals. Go also allows task.Task{1, "Buy milk", false, time.Now()}, which sets the fields by position. This breaks as soon as someone adds or reorders a field, and go vet warns about it for types from other packages.

Step 3: Use a constructor function

Look at New in task.go. Go has no special constructor syntax. By convention, a function named New (or NewSomething) creates a value and sets it up correctly.

New sets CreatedAt to the current time, so callers cannot forget to do it. Add this to play/main.go:

open := task.New(3, "Learn structs")
fmt.Println(task.Format(open))
fmt.Println(open.CreatedAt.Format("2006-01-02 15:04"))

The output is similar to this:

3. [ ] Learn structs
2026-10-16 21:35

The odd date format. Go formats dates with a fixed example date: 2006-01-02 15:04:05. You write how that exact moment should look, and Go formats your date the same way. "2006-01-02" gives year, month, and day. "15:04" gives hours and minutes. The numbers are easy to remember as 1, 2, 3, 4, 5, 6: month 01, day 02, hour 03 (15 on a 24-hour clock), minute 04, second 05, year 06.

Step 4: Structs are copied

Add this to play/main.go:

a := task.New(4, "Same")
b := a
b.Title = "Changed"
fmt.Println(a.Title, "/", b.Title)

Output:

Same / Changed

b := a copies the whole struct. After the copy, a and b are two separate values. A change to one does not affect the other.

The same thing happens when you pass a struct to a function. The function gets its own copy. So this function cannot change the caller's task:

func Complete(t Task) Task {
	t.Done = true
	return t
}

It changes its own copy, so it returns the changed copy to the caller. Add Complete to task.go, then add this to play/main.go:

finished := task.Complete(open)
fmt.Println(task.Format(open))
fmt.Println(task.Format(finished))

Output:

3. [ ] Learn structs
3. [x] Learn structs

The original task is still open. Copying makes code predictable, because a function cannot surprise you by changing your values. In Module 3, you learn pointers, which let a function change the original value when you want that.

Step 5: Use Task in the CLI

Now update main.go. In runAdd, replace the last line with:

t := task.New(1, title)
fmt.Println("Added:", task.Format(t))
fmt.Println("Created at:", t.CreatedAt.Format("2006-01-02 15:04"))

In runDone, replace the last line with:

t := task.Complete(task.New(id, "(title comes later in Module 2)"))
fmt.Println("Completed:", task.Format(t))

Build and run:

go build
./tasker add Buy milk
./tasker done 3

The output is similar to this:

Added: 1. [ ] Buy milk
Created at: 2026-10-16 21:35
Completed: 3. [x] (title comes later in Module 2)

The output looks almost the same as before. The difference is inside the code. All task data now travels as one Task value, and the rules for creating a task live in one place, the task package.

You can delete the play folder now, or keep it for experiments.

Why it works: types describe your domain

A struct gives a name to an idea from your problem. When you read func Format(t Task) string, you know exactly what the function works with. When you add a field to Task, such as a due date, no function signature has to change.

Go uses structs for almost everything. You will see them for configuration, for HTTP requests, for database rows, and for log entries. In Part 2, the same Task struct becomes the JSON your API sends and the row your database stores.

Exercises

  1. Add a priority. Add a Priority int field to Task. Change Format so it adds (!) to the end of the line when Priority is greater than 0.
  2. Print it two ways. Create a task with New and print it with %v and then with %+v. Which one is easier to read when you debug?
  3. Rename a task. Write a function Rename(t Task, title string) Task that returns a copy with the new title. Show that the original task keeps its old title.
  4. Format the age. Use time.Since(t.CreatedAt) to print how long ago a task was created. Hint: time.Since returns a time.Duration, which you can print with fmt.Println.
  5. Date formats. Print a task's CreatedAt in three formats: 02.01.2006, Jan 2, 2006, and 15:04:05.

Checkpoint

You are ready for the next lesson if you can:

  • Define a struct type with exported fields
  • Create a struct with a literal that uses field names
  • Say the zero value of each field in Task
  • Explain why Complete returns a Task instead of changing its parameter
  • Format a time.Time value with a layout string