JavaRush /Courses /Go SELF /type X struct { ... } — fields, zero values

type X struct { ... } — fields, zero values

Go SELF
Level 21, Lesson 0
Available

1. Struct: why you need it and how to declare it

If you have ever caught yourself thinking, “I’ve got id here, title here, done here, and the main thing is not to mix them up,” congratulations: you have almost invented a struct. You need a struct when several values logically belong to one object. For example, a task in a TODO list is not three independent variables but one entity, “Task,” which has fields.

Imagine the most “honest” option without a struct: we store task data in separate variables.

package main

import "fmt"

func main() {
	id := 1
	title := "Buy milk"
	done := false

	fmt.Println(id, title, done) // 1 Buy milk false
}

Does it work? Yes. Is it convenient? Right up until the first time you have 10 tasks and start inventing “id1/title1/done1,” then “id2/title2/done2”… and then you quietly cry and go live in the woods.

Declaring your own type: type Task struct { ... }

Now we move to the main idea: describe a new data type that combines several fields. In Go this is done with the keyword type, and inside we write struct { ... }. It is like creating your own “container” for data, where each field has a name and a type. You end up with one type—and then one variable of that type stores everything at once.

Here is the Task struct declaration:

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func main() {
	var t Task
	fmt.Printf("%T\n", t) // main.Task
}

Here it is important to feel the difference: Task is the type name, while t is a variable of that type.

To make it easier on your brain, you can think of it like this:

Phrase What it means
type Task struct { ... }
“I declare a new type ‘Task’”
var t Task
“I create a variable t that can hold a whole Task”
t.Title
“I access the Title field inside t”

Struct fields: name and type

When you describe a struct, each field is written as Name Type. It looks boring, but this is useful boring: the code becomes readable and predictable. Fields are like labeled cells in one box. Unlike arrays/slices, you do not access them by index; you access them by meaning: Title, Done, ID.

Let’s add a bit more meaning by printing the fields. For now we use var t Task, i.e., we create a struct “by default.”

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func main() {
	var t Task
	fmt.Printf("ID=%d Title=%q Done=%v\n", t.ID, t.Title, t.Done)
	// ID=0 Title="" Done=false
}

Notice the output: the fields are not “empty”—they have the zero value of each type. This is a core Go idea.

2. Zero value and working with fields

Zero value of a struct

With zero value, beginners often have a small philosophical crisis: “I didn’t assign anything—why is there something there?” The answer: in Go every variable always has a value. If you did not set your own, you get the default value. For a struct this works recursively: the zero value of a struct is a struct where each field equals the zero value of its type.

A mini table worth remembering:

Type Zero value
int
0
string
""
bool
false
[]T
nil
map[K]V
nil
*T
nil

And a struct is just the “sum” of these rules. One more time: var t Task creates a ready variable t that already has t.ID, t.Title, t.Done (it is just that for now they equal the defaults).

Accessing fields via .: read and write

Now to the most practical part: how to use a struct. To access fields you use the selector . (dot). You can read the dot as “inside.” That is, t.Title means “inside t, the Title field.” You can read, you can write—the syntax is the same.

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func main() {
	var t Task
	t.ID = 1
	t.Title = "Read Go spec (or at least pretend)"
	t.Done = false

	fmt.Println(t.ID, t.Title, t.Done) // 1 Read Go spec (or at least pretend) false
}

It looks too simple—and that is good. In Go, many things are exactly like this: “honest” and straightforward.

Struct and pointer to a struct

You already know &x and *T. So we can show a useful technique: if we want to modify a task inside a function, we pass a pointer *Task. At the same time, field access still looks like p.Title, without an explicit (*p).Title —the compiler will do the dereference for you.

package main

import "fmt"

type Task struct {
	Title string
	Done  bool
}

func markDone(t *Task) {
	t.Done = true
}

func main() {
	var x Task
	x.Title = "Wash the dishes"
	markDone(&x)

	fmt.Println(x.Title, x.Done) // Wash the dishes true
}

The key here is not to go too deep: for now it is enough to remember that &x gives an address, and if a function accepts *Task, then it can modify the original object.

3. Exported vs unexported fields: Title vs title

Now we take a step that may look “about style,” but in reality it is about architecture and code safety. In Go, a name starting with an uppercase letter means it is exported (accessible from other packages). A name starting with a lowercase letter is unexported (visible only within the current package). This applies to functions, types, and struct fields.

So far we have package main everywhere, so we are “inside one package” and can touch everything. But it is still important to understand this rule now so you do not get surprised later.

package main

import "fmt"

type Task struct {
	Title string
	done  bool
}

func main() {
	var t Task
	t.Title = "Public title"
	t.done = true // inside package main this is allowed

	fmt.Println(t.Title, t.done) // Public title true
}

The key idea: export is a boundary. As the author of a type, you decide what can be modified from the outside and what cannot. Sometimes it is useful to hide fields so nobody can write “nonsense” there directly.

There is also a practical effect in the standard library: for example, when encoding structs, some packages work only with exported fields. In particular, encoding/gob explicitly notes that only exported struct fields are encoded.

4. Example: a “task” as one entity

Let’s put together a small piece of “live” code. For now we do not use struct literals of the form Task{...} —that will be the next lecture. Here we work from the zero value: create a variable, fill the fields, and then use it. This style, by the way, often helps beginners because everything happens step by step and you can see it clearly.

Let’s make a function that creates a task and returns it by value.

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func makeTask(id int, title string) Task {
	var t Task
	t.ID = id
	t.Title = title
	t.Done = false
	return t
}

func main() {
	task := makeTask(1, "Learn structs")
	fmt.Println(task.ID, task.Title, task.Done) // 1 Learn structs false
}

Notice a nice effect: we now have one value task that can be passed around, stored in a slice []Task, returned from functions—and we do not have to carry id/title/done separately as three independent things.

5. Zero value: safe, but not always valid

Now comes an important note that often saves hours of debugging. In Go, zero value is a language guarantee: a variable is always in a predictable state. But for a “domain model” (for example, a “task”), the zero value can be weird: ID=0, Title="" are technically valid values, but semantically they may mean “someone forgot to fill in the task.”

Imagine you display a task list and see an empty title. The program did not crash, there are no errors, the compiler is happy—and the user stares at emptiness and also kind of crashes, but morally. So it is important to separate: zero value helps the program be stable, but data correctness rules we set separately (later in this course day there will be a topic about invariants and validation).

A small diagram of how this usually looks in a programmer’s head:

flowchart TD
    A[There is a variable of type Task] --> B[It always has a value]
    B --> C[If not initialized, it is the zero value]
    C --> D[Technically safe]
    D --> E[But not always semantically valid]

6. Common mistakes

Mistake #1: confusing the type and the variable.
A very common story: a student writes Task.Title = "..." and is surprised. Task is a type name; a type has no concrete data. The data lives in a variable: var t Task, and only then t.Title = "...". If you keep saying to yourself, “a type is a form, a variable is a concrete object,” it becomes easier.

Mistake #2: expecting zero value to mean “everything is filled in correctly.”
Zero value is not a magical auto-filler. You can absolutely end up with a task where ID=0 and Title is empty, and the program will keep working “as if nothing happened.” If your data must satisfy rules (for example, “title is not empty”), you have to validate those rules separately.

Mistake #3: making all fields exported “just in case.”
When everything starts with an uppercase letter, you are essentially saying, “Please change whatever you want, I don’t mind.” In a small example this is not scary, but over time it leads to code where any part of the program can ruin the state of your entities. It is better to start with fields as closed as possible and open only what is truly needed.

Mistake #4: giving fields meaningless names.
A int, B string, X bool is fine in 5-minute exercises, but bad in code you will read a week later (spoiler: in a week you will be a different person and remember nothing). Field names should say “what it is”: ID, Title, Done. If a field is about a date, use CreatedAt, not T (although the letter T in Go is sometimes truly too popular).

Mistake #5: trying to “compare to zero” the whole struct as one variable.
A struct does not have a single “zero number” like an int. You need to check specific fields: t.ID == 0, t.Title == "". Beginners sometimes look for a “universal emptiness check,” but in Go the thought is usually simple: check what actually matters semantically.

1
Task
Go SELF, level 21, lesson 0
Locked
Profile snapshot
Profile snapshot
1
Task
Go SELF, level 21, lesson 0
Locked
Message route
Message route
1
Task
Go SELF, level 21, lesson 0
Locked
Score Counter
Score Counter
1
Task
Go SELF, level 21, lesson 0
Locked
Wallet internals
Wallet internals
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION