Skip to main content

What is a Struct?

A struct is a composite data type that groups together variables under a single name. Each variable in a struct is called a field. Structs let you create your own custom types to model real-world entities.
type Person struct {
    Name     string
    Lastname string
    Age      int
}

Why Use Structs?

Before structs, you might manage related data with separate variables:
// Without structs - messy and error-prone
var name1, lastname1 string
var age1 int

var name2, lastname2 string
var age2 int

name1, lastname1, age1 = "Pablo", "Picasso", 91
name2, lastname2, age2 = "Sigmund", "Freud", 83
With structs, data is organized and type-safe:
// With structs - clean and organized
type Person struct {
    Name     string
    Lastname string
    Age      int
}

picasso := Person{Name: "Pablo", Lastname: "Picasso", Age: 91}
freud := Person{Name: "Sigmund", Lastname: "Freud", Age: 83}

Defining Struct Types

Basic Definition

type Movie struct {
    Title  string
    Genre  string
    Rating int
}

type Rental struct {
    Address string
    Rooms   int
    Size    int
    Price   int
}

Anonymous Structs

You can create structs without defining a type:
// Anonymous struct
person := struct {
    name string
    age  int
}{
    name: "Alice",
    age:  30,
}

fmt.Printf("%+v\n", person)  // {name:Alice age:30}
Anonymous structs are useful for one-off data structures, but named types are better for reusability and clarity.

Creating Struct Values

Struct Literals

type Person struct {
    Name     string
    Lastname string
    Age      int
}

// Method 1: Named fields (recommended)
picasso := Person{
    Name:     "Pablo",
    Lastname: "Picasso",
    Age:      91,
}

// Method 2: Positional (must include all fields in order)
freud := Person{"Sigmund", "Freud", 83}

// Method 3: Partial initialization (others are zero values)
partial := Person{
    Name: "Alice",
}
fmt.Printf("%+v\n", partial)  // {Name:Alice Lastname: Age:0}
Always use named fields for clarity and maintainability. Positional initialization breaks if you reorder fields.

Zero Values

var person Person
// All fields get zero values:
// Name: ""
// Lastname: ""
// Age: 0

fmt.Printf("%+v\n", person)  // {Name: Lastname: Age:0}

The var Keyword

// Declare and initialize later
var freud Person
freud.Name = "Sigmund"
freud.Lastname = "Freud"
freud.Age = 83

fmt.Printf("%#v\n", freud)
// main.Person{Name:"Sigmund", Lastname:"Freud", Age:83}

Accessing Fields

Use the dot notation to access struct fields:
picasso := Person{
    Name:     "Pablo",
    Lastname: "Picasso",
    Age:      91,
}

// Read fields
fmt.Println(picasso.Name)     // "Pablo"
fmt.Println(picasso.Age)      // 91

// Modify fields
picasso.Age = 92
fmt.Println(picasso.Age)      // 92

// Use in expressions
fmt.Printf("%s's age is %d\n", picasso.Lastname, picasso.Age)
// Output: Picasso's age is 92

Comparing Structs

Structs are comparable if all their fields are comparable:
type Point struct {
    X, Y int
}

p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{2, 1}

fmt.Println(p1 == p2)  // true (all fields equal)
fmt.Println(p1 == p3)  // false (different values)
Structs containing slices, maps, or functions are not comparable:
type NotComparable struct {
    Name  string
    Tags  []string  // Slice - not comparable
}

a := NotComparable{Name: "test", Tags: []string{"a"}}
b := NotComparable{Name: "test", Tags: []string{"a"}}
// fmt.Println(a == b)  // ❌ Compile error

Copying Structs

Structs are value types. Assignment creates a copy:
original := Person{Name: "Alice", Age: 30}
copy := original

copy.Age = 31

fmt.Println(original.Age)  // 30 (unchanged)
fmt.Println(copy.Age)      // 31 (modified)

Struct Embedding

Embedding lets you include one struct inside another, promoting the embedded fields:
type Text struct {
    Title string
    Words int
}

type Book struct {
    Text  // Embedded struct
    ISBN  string
}

// Create a book
moby := Book{
    Text: Text{
        Title: "Moby Dick",
        Words: 206052,
    },
    ISBN: "102030",
}

// Access embedded fields directly
fmt.Println(moby.Title)  // "Moby Dick" (promoted from Text)
fmt.Println(moby.Words)  // 206052 (promoted from Text)

// Or access via embedded field name
fmt.Println(moby.Text.Title)  // "Moby Dick"
fmt.Println(moby.Text.Words)  // 206052

// Access Book's own fields
fmt.Println(moby.ISBN)  // "102030"

Field Name Conflicts

If an embedded struct has a field with the same name as the outer struct, you must qualify it:
type Text struct {
    Title string
    Words int
}

type Book struct {
    Text
    Title string  // Conflicts with Text.Title
    ISBN  string
}

moby := Book{
    Text: Text{
        Title: "Moby Dick (from Text)",
        Words: 206052,
    },
    Title: "Moby Dick (from Book)",
    ISBN:  "102030",
}

fmt.Println(moby.Title)       // "Moby Dick (from Book)"
fmt.Println(moby.Text.Title)  // "Moby Dick (from Text)"
The outer field “shadows” the embedded field. Use the full path moby.Text.Title to access the embedded field.

Pointers to Structs

Use pointers when you want to modify the original struct or avoid copying large structs:
type Person struct {
    Name string
    Age  int
}

// Create a pointer to a struct
person := &Person{Name: "Alice", Age: 30}

// Access fields (Go auto-dereferences)
fmt.Println(person.Name)  // "Alice"
person.Age = 31            // Modifies original

// Equivalent explicit syntax
fmt.Println((*person).Name)
(*person).Age = 32

When to Use Pointers

Use Pointers When

  • You need to modify the struct
  • The struct is large (avoid copying)
  • You want to share state
  • Implementing methods that modify the receiver

Use Values When

  • The struct is small
  • You want immutability
  • The struct represents a value (Point, Color, etc.)
  • No need to modify the original

JSON Encoding and Decoding

Structs work seamlessly with JSON encoding:
import "encoding/json"

type User struct {
    Name        string `json:"username"`
    Password    string `json:"-"`                    // Omitted from JSON
    Permissions map[string]bool `json:"perms,omitempty"`  // Omit if empty
}

users := []User{
    {"Alice", "1234", nil},
    {"Admin", "admin", map[string]bool{"write": true}},
}

// Encode to JSON
data, err := json.MarshalIndent(users, "", "  ")
if err != nil {
    fmt.Println(err)
    return
}

fmt.Println(string(data))
Output:
[
  {
    "username": "Alice"
  },
  {
    "username": "Admin",
    "perms": {
      "write": true
    }
  }
]

Struct Tags

Struct tags provide metadata for JSON encoding:
type User struct {
    Name     string `json:"username"`           // Rename field
    Password string `json:"-"`                  // Omit from JSON
    Email    string `json:"email,omitempty"`    // Omit if empty
    Age      int    `json:"age,string"`         // Encode number as string
}
Common tags:
  • json:"name" - Rename field in JSON
  • json:"-" - Skip field entirely
  • json:",omitempty" - Omit field if it has a zero value
  • json:",string" - Encode/decode as string

Decoding JSON

jsonData := `{
  "username": "Alice",
  "email": "alice@example.com"
}`

var user User
err := json.Unmarshal([]byte(jsonData), &user)
if err != nil {
    fmt.Println(err)
    return
}

fmt.Printf("%+v\n", user)
// {Name:Alice Password: Email:alice@example.com Age:0}

Nested Structs

Structs can contain other structs:
type Address struct {
    Street  string
    City    string
    ZipCode string
}

type Person struct {
    Name    string
    Age     int
    Address Address  // Nested struct
}

person := Person{
    Name: "Alice",
    Age:  30,
    Address: Address{
        Street:  "123 Main St",
        City:    "Springfield",
        ZipCode: "12345",
    },
}

fmt.Println(person.Address.City)  // "Springfield"

Struct Slices and Maps

Slice of Structs

type Movie struct {
    Title  string
    Genre  string
    Rating int
}

movies := []Movie{
    {Title: "Inception", Genre: "Sci-Fi", Rating: 9},
    {Title: "The Matrix", Genre: "Sci-Fi", Rating: 9},
    {Title: "Interstellar", Genre: "Sci-Fi", Rating: 8},
}

for _, movie := range movies {
    fmt.Printf("%s: %d/10\n", movie.Title, movie.Rating)
}

Map of Structs

type User struct {
    Name  string
    Email string
}

users := map[string]User{
    "alice": {Name: "Alice", Email: "alice@example.com"},
    "bob":   {Name: "Bob", Email: "bob@example.com"},
}

fmt.Println(users["alice"].Name)  // "Alice"

Common Patterns

Constructor Functions

type Rectangle struct {
    Width  float64
    Height float64
}

// Constructor function
func NewRectangle(width, height float64) Rectangle {
    return Rectangle{
        Width:  width,
        Height: height,
    }
}

rect := NewRectangle(10, 5)

Options Pattern

type Server struct {
    Host string
    Port int
}

func NewServer(host string, port int) *Server {
    return &Server{
        Host: host,
        Port: port,
    }
}

server := NewServer("localhost", 8080)

Builder Pattern

type User struct {
    Name  string
    Email string
    Age   int
}

type UserBuilder struct {
    user User
}

func (ub *UserBuilder) Name(name string) *UserBuilder {
    ub.user.Name = name
    return ub
}

func (ub *UserBuilder) Email(email string) *UserBuilder {
    ub.user.Email = email
    return ub
}

func (ub *UserBuilder) Age(age int) *UserBuilder {
    ub.user.Age = age
    return ub
}

func (ub *UserBuilder) Build() User {
    return ub.user
}

// Usage
user := (&UserBuilder{}).
    Name("Alice").
    Email("alice@example.com").
    Age(30).
    Build()

Printing Structs

Different formatting options:
person := Person{Name: "Alice", Age: 30}

fmt.Printf("%v\n", person)   // {Alice 30}
fmt.Printf("%+v\n", person)  // {Name:Alice Age:30} (includes field names)
fmt.Printf("%#v\n", person)  // main.Person{Name:"Alice", Age:30} (Go syntax)

Key Takeaways

Structs group related data into a single custom type with named fields.
Structs are value types. Assignment and passing copies the entire struct.
Uninitialized struct fields get their type’s zero value automatically.
Embed structs to promote fields and create composition hierarchies without traditional inheritance.
Use struct tags to control JSON encoding/decoding behavior.
Always use named fields in struct literals for clarity and maintainability.

See Also

  • Maps - Often used with structs for lookups
  • Slices - Commonly hold collections of structs
  • Methods - Add behavior to struct types
  • JSON Package - Encoding and decoding structs

Build docs developers (and LLMs) love