Skip to main content
Go beginner Lesson 11 of 25

Structs in Go

Define structs, use struct literals and embedding, add methods, work with struct tags, and understand anonymous fields.

Defining Structs

A struct is Go’s primary tool for grouping related data into a named type. Where other languages use classes to bundle data and behavior, Go separates the two: a struct holds data, and methods are attached to it separately. This makes the data layout explicit and the relationship between types easy to follow without navigating a class hierarchy.

type Person struct {
    Name  string
    Age   int
    Email string
}

Struct Literals

Struct literals create a value of the struct type. The named-field form is strongly preferred — it is resilient to field reordering, self-documenting, and clearly shows which fields are being set. The positional form is fragile and should be avoided except in test code with very short structs. Any field not specified in the literal gets its zero value automatically.

// Named fields — preferred: readable and order-independent
alice := Person{
    Name:  "Alice",
    Age:   30,
    Email: "alice@example.com",
}

// Positional — fragile: breaks silently if fields are reordered
bob := Person{"Bob", 25, "bob@example.com"}

// Zero-value struct — all fields are zero-initialized
var empty Person
fmt.Println(empty) // { 0 }

// Pointer to struct — Go auto-dereferences pointers to structs, so p.Name works directly
p := &Person{Name: "Charlie", Age: 35}
fmt.Println(p.Name) // Charlie

Methods on Structs

A method is a function with a receiver — an extra parameter that binds the function to a specific type. The receiver can be a value (receives a copy) or a pointer (receives the actual struct). The key rule is: use a pointer receiver when the method needs to modify the struct or when the struct is large enough that copying it is expensive.

type Rectangle struct {
    Width  float64
    Height float64
}

// Value receiver — works on a copy; the original is not modified
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Pointer receiver — modifies the actual struct, not a copy
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println(rect.Area())      // 50
    fmt.Println(rect.Perimeter()) // 30

    rect.Scale(2)
    fmt.Println(rect.Area())      // 200 — modified in place via pointer receiver
}

Rule of thumb: Use pointer receivers when the method modifies the struct or when the struct is large (avoids copying). Be consistent — if any method uses a pointer receiver, all methods should.

Constructor Functions

Go has no special constructor syntax, but the community convention is to write a New... function that returns a properly initialized value. This is better than bare struct literals for exported types because it lets you enforce invariants, set defaults, and change the internal representation later without breaking callers.

type Server struct {
    host    string
    port    int
    timeout time.Duration
}

// NewServer is the conventional constructor — sets defaults and validates input
func NewServer(host string, port int) *Server {
    return &Server{
        host:    host,
        port:    port,
        timeout: 30 * time.Second, // default that callers don't need to specify
    }
}

func (s *Server) Addr() string {
    return fmt.Sprintf("%s:%d", s.host, s.port)
}

s := NewServer("localhost", 8080)
fmt.Println(s.Addr()) // localhost:8080

Struct Embedding

Embedding allows one struct to include another as an anonymous field. The embedded struct’s fields and methods are promoted to the outer struct — you can access them as if they were defined directly on the outer type. This is Go’s primary mechanism for code reuse and achieves most of what inheritance does in other languages, without the tight coupling.

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal        // embedded — no field name, just the type
    Breed  string
}

// Dog can override the promoted method with its own implementation
func (d Dog) Speak() string {
    return d.Name + " says: Woof!"
}

d := Dog{
    Animal: Animal{Name: "Rex"},
    Breed:  "Labrador",
}

fmt.Println(d.Name)    // Rex   — promoted from Animal, accessible directly
fmt.Println(d.Speak()) // Rex says: Woof! — Dog's method takes precedence

// The embedded struct is still accessible explicitly when needed
fmt.Println(d.Animal.Speak()) // Rex makes a sound

Anonymous Fields

Embedding is not limited to structs you define — you can embed any named type. A common pattern is embedding a shared “mixin” struct to add common fields (like timestamps or audit info) to multiple domain types without duplicating the field declarations.

type Timestamps struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}

type User struct {
    ID    int
    Email string
    Timestamps // embedded — User now has CreatedAt and UpdatedAt directly
}

u := User{
    ID:    1,
    Email: "alice@example.com",
    Timestamps: Timestamps{
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    },
}

fmt.Println(u.CreatedAt) // accessible directly, no need for u.Timestamps.CreatedAt

Struct Tags

Struct tags are raw string literals in backticks that attach metadata to fields. They are invisible to the compiler but readable at runtime via the reflect package. The encoding/json package is the most common consumer of tags — it uses them to control how fields are named in JSON output, whether to omit empty fields, and whether to exclude a field entirely.

type Article struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Body      string    `json:"body,omitempty"` // omit from JSON output if empty
    CreatedAt time.Time `json:"created_at"`
    Internal  string    `json:"-"`              // always exclude from JSON
}

article := Article{
    ID:        1,
    Title:     "Go Structs",
    CreatedAt: time.Now(),
    Internal:  "admin-only",
}

data, _ := json.Marshal(article)
fmt.Println(string(data))
// {"id":1,"title":"Go Structs","created_at":"2024-01-15T10:00:00Z"}
// Body is omitted (empty string), Internal is omitted (tagged with "-")

// Multiple tags let different packages each read their own metadata
type DBRecord struct {
    ID   int    `json:"id" db:"id" validate:"required"`
    Name string `json:"name" db:"name" validate:"min=2,max=100"`
}

Comparing Structs

Structs are comparable with == and != if all their fields are comparable types. Two struct values are equal when all their corresponding fields are equal. Structs that contain slices, maps, or functions are not comparable — attempting to compare them is a compile error.

type Point struct{ X, Y int }

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

fmt.Println(p1 == p2) // true — all fields match
fmt.Println(p1 == p3) // false — fields differ

// Structs with non-comparable fields cannot be compared with ==
type Container struct {
    Items []int // slices are not comparable
}
// c1 == c2 would be a compile error

Practical Example — Domain Model

This example shows how structs, embedding, tags, and methods combine in a realistic domain model. The Customer type embeds Address directly, uses JSON tags for serialization, and keeps some fields unexported to enforce encapsulation.

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
    Zip    string `json:"zip"`
}

type Customer struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Address   Address   `json:"address"`
    CreatedAt time.Time `json:"created_at"`
    tags      []string  // unexported — not included in JSON output
}

func NewCustomer(id int, name, email string) *Customer {
    return &Customer{
        ID:        id,
        Name:      name,
        Email:     email,
        CreatedAt: time.Now(),
    }
}

func (c *Customer) AddTag(tag string) {
    c.tags = append(c.tags, tag)
}

func (c *Customer) HasTag(tag string) bool {
    for _, t := range c.tags {
        if t == tag {
            return true
        }
    }
    return false
}

// String implements fmt.Stringer — controls how the customer appears in print statements
func (c Customer) String() string {
    return fmt.Sprintf("Customer{ID:%d, Name:%s}", c.ID, c.Name)
}

func main() {
    cust := NewCustomer(1, "Alice", "alice@example.com")
    cust.Address = Address{
        Street: "123 Gopher Lane",
        City:   "Go City",
        Zip:    "12345",
    }
    cust.AddTag("premium")
    cust.AddTag("verified")

    fmt.Println(cust)                   // Customer{ID:1, Name:Alice}
    fmt.Println(cust.HasTag("premium")) // true

    data, _ := json.MarshalIndent(cust, "", "  ")
    fmt.Println(string(data))
}

Frequently Asked Questions

Does Go have classes?
No. Go uses structs with methods instead of classes. A struct holds data; you attach behavior by defining methods with a receiver of that struct type. This achieves the same result without class-based inheritance.
What is struct embedding?
Embedding lets one struct include another by value without a field name. The embedded struct's fields and methods are promoted to the outer struct, enabling a form of composition that resembles inheritance.
What are struct tags used for?
Struct tags are metadata strings attached to fields. They are read at runtime via reflection. The encoding/json package uses them to control JSON serialization: json:"name,omitempty" sets the JSON key and omits the field when empty.