Free Ebook cover Go (Golang) Fundamentals: Simple, Fast Programs for Beginners

Go (Golang) Fundamentals: Simple, Fast Programs for Beginners

New course

12 pages

Structs and Methods in Go: Modeling Data and Behavior

Capítulo 7

Estimated reading time: 7 minutes

+ Exercise

1) Defining Structs: Fields, Tags, and Zero Values

A struct groups related data into a single type. It is Go’s primary way to model “things” with multiple properties.

Basic struct definition

package main

type User struct {
	ID    int
	Name  string
	Email string
	Admin bool
}

Field names that start with an uppercase letter are exported (visible to other packages and to encoders like encoding/json). Lowercase field names are unexported.

Zero values for struct fields

When you declare a struct variable without initializing it, each field gets its type’s zero value:

  • int0
  • string""
  • boolfalse
  • pointers/slices/maps/functions/interfaces → nil
var u User
// u.ID == 0, u.Name == "", u.Email == "", u.Admin == false

Struct tags (briefly): JSON field names and options

Tags are string metadata attached to fields. The most common beginner use is JSON marshaling/unmarshaling.

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email,omitempty"`
	Admin bool   `json:"admin"`
}
  • json:"id" sets the JSON key name.
  • omitempty omits the field if it has a zero value (e.g., empty string).

Tags are read by packages via reflection; you don’t “use” them directly in normal code.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

2) Constructing Struct Values: Literals, Pointers, and new

Composite literals (recommended for clarity)

You can create a struct value with a composite literal. Prefer named fields for readability and stability when fields change.

u := User{
	ID:    1,
	Name:  "Ava",
	Email: "ava@example.com",
	Admin: false,
}

You can also use positional fields, but it is easier to break when the struct changes:

// Avoid in most codebases unless the struct is tiny and stable.
u2 := User{1, "Ava", "ava@example.com", false}

Pointers to structs and automatic address-taking

Often you want a pointer so methods can mutate the struct, or to avoid copying large structs.

up := &User{
	ID:   2,
	Name: "Noah",
}

Go lets you access fields through a pointer without explicit dereferencing:

up.Name = "Noah R." // same as (*up).Name = "Noah R."

new(T) vs &T{} vs T{}

  • var u User or u := User{} creates a value with zero values.
  • p := new(User) allocates a zero-valued User and returns *User.
  • p := &User{...} allocates and initializes, returning *User.
u := User{}        // value
p1 := new(User)    // *User, all fields zero
p2 := &User{Name: "Mia"} // *User with Name set

In practice: use User{...} for values and &User{...} for pointers. Use new when you specifically want a pointer to a zero value without setting fields.

3) Methods: Attaching Behavior to Data

A method is a function with a receiver. Receivers let you write behavior “on” a type.

Defining methods

type User struct {
	ID    int
	Name  string
	Email string
}

func (u User) DisplayName() string {
	if u.Name == "" {
		return "(unknown)"
	}
	return u.Name
}

This method uses a value receiver (u User), so it operates on a copy of the struct.

Pointer receivers for mutation

If a method needs to modify the receiver, use a pointer receiver:

func (u *User) SetEmail(email string) {
	u.Email = email
}

Calling it is still simple—Go automatically takes the address when possible:

u := User{Name: "Ava"}
u.SetEmail("ava@example.com") // compiler uses &u

Method sets (what methods are available on values vs pointers)

  • A value of type T has methods with receiver (t T).
  • A pointer of type *T has methods with receiver (t T) and (t *T).

This matters when a type needs to satisfy an interface: if an interface requires a method that has a pointer receiver, only *T satisfies it (not T).

Guidelines for choosing receivers

  • Use a pointer receiver when the method mutates the receiver.
  • Use a pointer receiver when copying the value would be expensive (large structs).
  • Use a value receiver for small, immutable-like data where copying is cheap and you want simpler semantics.
  • Be consistent: if some methods need pointer receivers, many teams make all methods pointer receivers for that type to avoid confusion.

4) Embedding: Composition Over Inheritance

Go does not have class inheritance. Instead, it encourages composition: build types from other types. Embedding is a special form of composition where an anonymous field’s members are promoted.

Embedding a struct type

type Audit struct {
	CreatedAt string
	UpdatedAt string
}

type Task struct {
	Audit // embedded
	ID    int
	Title string
}

Because Audit is embedded, its fields are promoted and can be accessed directly:

t := Task{ID: 1, Title: "Write docs"}
t.CreatedAt = "2026-01-16" // promoted field (actually t.Audit.CreatedAt)

Promoted methods

Methods on an embedded type are also promoted, so the outer type can call them as if they were its own (subject to method sets and pointer/value rules).

type Audit struct {
	CreatedAt string
	UpdatedAt string
}

func (a *Audit) Touch(now string) {
	a.UpdatedAt = now
}

type Task struct {
	Audit
	Title string
}

// Task gets Touch promoted (when you have *Task or Task depending on receiver rules)

When embedding is appropriate

  • When the embedded type represents a reusable “part” of the outer type (e.g., auditing fields, common configuration, shared behavior).
  • When promoted fields/methods improve ergonomics without hiding meaning.
  • Avoid embedding just to “inherit” a large API; it can make the outer type’s surface area confusing.

If you want composition without promotion, use a named field instead:

type Task struct {
	AuditInfo Audit // not embedded; access via t.AuditInfo.CreatedAt
	Title     string
}

5) Small Project: A User Type with Validation, Formatting, and JSON

This mini project models a user with a few rules:

  • Name is required.
  • Email is optional, but if present it must contain @.
  • Provide methods to validate and format output.
  • Show JSON marshaling using tags.

Step 1: Define the struct with JSON tags

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
)

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email,omitempty"`
	Admin bool   `json:"admin"`
}

Step 2: Add a validation method

Validation typically does not need to mutate the receiver, so a value receiver is fine. If you plan to cache results or normalize fields during validation, switch to a pointer receiver.

func (u User) Validate() error {
	if strings.TrimSpace(u.Name) == "" {
		return errors.New("name is required")
	}
	if u.Email != "" && !strings.Contains(u.Email, "@") {
		return errors.New("email must contain @")
	}
	return nil
}

Step 3: Add a formatting method

This method returns a human-friendly string without changing the user.

func (u User) Label() string {
	role := "user"
	if u.Admin {
		role = "admin"
	}
	if u.Email == "" {
		return fmt.Sprintf("%s (#%d, %s)", u.Name, u.ID, role)
	}
	return fmt.Sprintf("%s <%s> (#%d, %s)", u.Name, u.Email, u.ID, role)
}

Step 4: Add a mutating method (pointer receiver)

Normalization is a common reason to use pointer receivers.

func (u *User) Normalize() {
	u.Name = strings.TrimSpace(u.Name)
	u.Email = strings.TrimSpace(strings.ToLower(u.Email))
}

Step 5: Use the type and marshal to JSON

This example shows a typical flow: construct, normalize, validate, then marshal.

func main() {
	u := &User{
		ID:    10,
		Name:  "  Ava  ",
		Email: "AVA@EXAMPLE.COM",
		Admin: true,
	}

	u.Normalize()
	if err := u.Validate(); err != nil {
		panic(err)
	}

	fmt.Println(u.Label())

	b, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}

Because of the JSON tags:

  • The output keys will be id, name, email, admin.
  • If Email is empty, it will be omitted due to omitempty.

Now answer the exercise about the content:

In Go, why would you choose a pointer receiver for a method on a struct?

You are right! Congratulations, now go to the next page

You missed! Try again.

Pointer receivers let methods modify the receiver’s fields and can avoid copying large structs. Value receivers operate on a copy and are better when no mutation is needed.

Next chapter

Interfaces in Go: Decoupling Code with Small, Focused Contracts

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.