AlgoMaster Logo

Struct Basics

Last Updated: May 17, 2026

10 min read

A struct is how you group related fields under one name in Go: a product has a code, a name, a price, and a stock count, and all four belong together. Without structs you'd carry those fields around as separate variables or stuff them into a map and lose all type safety. This lesson covers what a struct is, how to declare a struct type, the zero value of a struct, how to read and write fields, and why structs are a better fit than maps for fixed, heterogeneous records.

Why Structs Exist

Imagine you're tracking a single product in an online store. You need its code, its name, its price, and how many are in stock. Without structs, that's four parallel variables, and you have to keep them in sync by hand:

This works for one product. Now imagine a function that needs to print a product. It takes four parameters. A function that sells a product takes four parameters and returns four updated values. Multiply that across an application and you're carrying the same four-tuple everywhere, and any time you add a field (say, a category), every function signature has to change.

The next instinct is often to use a map:

That groups the fields into one value, which is progress. The problem is the map has no schema. The compiler doesn't know "price" is a float or that "stock" is an int. It doesn't even know the key "price" exists; misspelling it as "pric" gives you back the zero value silently. Every read needs a type assertion (product["price"].(float64)) before you can do arithmetic, and every typo is a runtime bug instead of a compile error.

A struct fixes both problems. It defines, at compile time, exactly which fields a value has and what type each field is. Misspelling a field is a compile error. Reading a field gives back a value of the right type, no assertion needed. And one named type can describe every product in the program.

The four loose variables (and the typo-prone map) have become one Product value with four well-typed fields. The compiler knows the layout, so it can catch mistakes early and generate efficient code that reads each field directly.

Declaring a Struct Type

You declare a struct type with type, the name of the type, the keyword struct, and a brace-enclosed list of fields. Each field has a name and a type.

The declaration creates a brand new named type called Product. From now on, Product is a type just like int or string, and you can declare variables, function parameters, and return values of that type.

The fields are listed one per line by convention. Each line is a name followed by a type, with no comma in between. You can also group fields of the same type on one line, which sometimes reads better for closely related data:

Here Street, City, and Country all share the type string. The grouped form is shorthand for declaring each one individually. It's a style choice, not a semantic difference.

A struct type lives at the file or package level. You typically declare it at the top of a file, outside any function, so any function in the package can use it. You can declare a struct type inside a function too, but the type is then only visible inside that function, which is rare in practice.

A struct type is a fixed list of named, typed slots. Each value of that type carries exactly those slots in exactly that order. Two struct types with different field names or different field types are different types, even if they look similar.

Field Types

A struct field can be any Go type. Basic types like string, int, float64, and bool are the common ones, but you can also use slices, maps, arrays, pointers, function types, interfaces, and other struct types.

The Tags field is a slice, and OnSale is a boolean. The struct just glues them together under one name. There's no special treatment for any field type.

A struct field can itself be a struct. This is how you model nested data, like a customer with an address:

Notice the dotted access: c.Address.Street. You walk into the outer struct's Address field, then into the inner Address struct's Street field. There's no special syntax. It's just one dot per level.

Nested structs compose naturally. A Customer has an Address, and you could have an Order that contains a Customer and a slice of Products. Each piece is its own type, and the outer types just combine them. This is how Go programs build up complex shapes from simple ones, without inheritance.

The Zero Value of a Struct

Every Go type has a zero value, and structs are no exception. The zero value of a struct is a value where every field is set to its own zero value. That means strings are "", numbers are 0, booleans are false, slices and maps are nil, and nested structs are themselves zero-valued.

When you write var p Product, Go allocates memory for the whole struct and fills every byte with zero. There's no "uninitialized" state to worry about, and you never read garbage from a freshly declared struct. That's a deliberate design choice in Go, and it works at every level: zero values cascade through nested structs all the way down.

The Customer is zero, and its nested Address is also zero. You don't have to initialize the inner struct separately. Reading c.Address.City returns "" because every field, at every depth, starts zeroed.

Zero-valued structs are often immediately useful. A var cart Cart can be the starting state of an empty shopping cart, a var orderStatus Status can be the "not yet placed" state, and you don't need a constructor to get there. The trade-off is that the zero value has to be meaningful for the type, which is something to think about when designing your structs. The standard library leans into this hard: sync.Mutex, bytes.Buffer, and many others are designed so the zero value is ready to use.

Declaring a Struct Variable

The simplest way to declare a struct variable is with var. It gives you a variable of the struct type with every field zeroed:

var p Product declares a new Product value. The compiler allocates space for the four fields and fills each with its zero value. The variable is ready to use immediately; there's no separate initialization step required before you can read or write its fields.

You can also declare multiple struct variables on separate lines. Each one is independent and starts with its own zero values:

Both book and mug are zero-valued Products. They're separate values in memory; modifying one doesn't affect the other.

There are other ways to create a struct (with literal syntax like Product{...} or with the new function), but var is the most direct form when you want a zero-valued struct to fill in field by field. We'll stick with var and field assignment for now.

Accessing and Setting Fields

You read and write a struct's fields with the dot operator: the variable name, a dot, and the field name. Reading a field gives back the field's current value with the field's declared type:

Each assignment writes into a specific field. Each read pulls back the value of that field. The compiler checks the field name and the type on every access, so p.Pric = 24.99 is a compile error (unknown field), and p.Stock = "twelve" is a compile error too (wrong type).

Fields can be used in any expression, exactly like a regular variable of the same type. You can do arithmetic on numeric fields, concatenate string fields, index slice fields, and pass fields to functions:

p.Price and p.Stock are used in arithmetic, and p.Code and p.Name are concatenated. There's no unwrapping step. A field reference is the value of that field.

You can also mutate a field by assigning to it, and that change sticks for as long as the struct variable lives:

p.Stock = p.Stock - 1 reads the current stock, subtracts one, and writes the result back into the same field. The same pattern works for any field type that supports the relevant operation.

Structs Are Value Types

Structs in Go are value types. Assigning one struct variable to another copies the whole struct: every field is copied into the new variable. Reading or modifying the copy never touches the original, and the reverse is also true.

The line copy := original doesn't make copy an alias for original. It makes a fresh Product value with the same field values, sitting in its own memory. Changes to copy.Price and copy.Stock only affect copy. The original is left exactly as it was.

The same value-copy happens when you pass a struct to a function or return it from one. The function receives its own copy:

The function modifies its own copy of p, but the caller's p is unchanged. Value semantics make this the default behavior. If you want the function to actually mutate the caller's struct, you pass a pointer instead. For now, just know that struct assignment and struct arguments copy everything.

The diagram shows two independent struct values. After the copy, original and copy live in separate memory. They start with identical field values, but they're not linked. Mutating one leaves the other untouched.

There's a subtle wrinkle when a struct contains a slice or a map. The struct's slice field is itself a small header (pointer, length, capacity) that points at a backing array. When you copy the struct, you copy the slice header but not the backing array, so both copies point at the same underlying data. Modifying an element through one copy is visible through the other. This is how slices and maps work everywhere in Go, not a struct quirk.

Struct vs Map: When to Use Which

Both structs and maps can hold a collection of named values, so when do you reach for each? The short version: structs are for fixed records with known fields of mixed types, and maps are for dynamic lookups where you don't know all the keys ahead of time.

A Product is a struct because every product has the same four fields: a code, a name, a price, a stock count. The fields are heterogeneous (string, string, float64, int), and you want the compiler to catch typos and type mismatches. The set of fields doesn't change at runtime.

A price list, on the other hand, is a map. You don't know in advance which product codes will be in the list. New products get added, old ones get removed, and every value is the same type (a price).

The product itself is a Product struct. The inventory across many products is a map[string]int. They're complementary, not competing.

Here's a side-by-side that captures when to pick which:

PropertyStructMap
Field setFixed at compile timeDynamic at runtime
Field typesCan differ per fieldAll values share one type
Typo catchesCompile errorSilent zero value
Memory layoutFields packed in fixed orderHash table, indirect
Iteration over fieldsNot built-in (use reflection)Built-in with range
Best forRecords: products, orders, customersLookups: code to price, name to count

A common mistake is using a map[string]any to model a record like a customer or a product. It compiles, but you lose type safety and pay a small overhead on every access. Pick a struct when the shape is fixed. Pick a map when the keys are data, not part of the schema.

There are also cases where you combine the two. A shopping cart might be a struct that holds a customer name, a discount code, and a map[string]int of product codes to quantities. The cart itself has a fixed shape, but the items inside it are a dynamic lookup.

The outer shape is a struct, because every cart has a customer, a discount, and a set of items. The items themselves are a map, because the set of products in any given cart is open-ended. This pattern (struct of fixed fields, with a map or slice for the variable part) shows up all over real Go code.

Summary

  • A struct groups a fixed list of named, typed fields under a single type. Use type Name struct { ... } to declare one.
  • Field types can be anything: basic types, slices, maps, other structs, function types, even interfaces. Nesting structs is how you model hierarchical data.
  • The zero value of a struct sets every field to its own zero value. var p Product gives you a fully zeroed struct, ready to use without explicit initialization.
  • Read and write fields with the dot operator: p.Price, p.Stock = 12. Misspelling a field or assigning a wrong type is a compile error.
  • Structs are value types. Assignment copies every field, and passing a struct to a function passes a copy. The two values are independent afterwards.
  • Pick a struct when the field set is fixed and the types differ. Pick a map when keys are data and values share one type. The two combine cleanly when you need both shapes.

The next lesson, Struct Literals & Initialization, shows the literal forms (Product{...}) that let you create a fully populated struct in one expression, including the rules for positional versus named fields and how to construct nested struct values without going through a chain of field assignments.