On this page

Structs and Methods

15 min read TextCh. 2 — Functions and Structures

Structs: Go's Way of Modeling Data

Go has no classes. Instead, it uses structs to group related data, and methods to associate behavior with that data. This model is simpler than Java's or C#'s class inheritance, but equally powerful when combined with interfaces and embedding.

If you know C or Rust, Go's structs will feel familiar. If you come from Java or Python, think of a struct as a class — but without inheritance.

Defining Structs

// Basic definition
type Person struct {
    Name     string
    LastName string
    Age      int
    Email    string
}

// Nested struct
type Address struct {
    Street  string
    City    string
    Country string
}

type Employee struct {
    Person              // embedding (composition)
    Company    string
    Salary     float64
    Address    Address  // named field (not embedding)
}

Names starting with an uppercase letter are exported (visible outside the package). Names starting with a lowercase letter are unexported (only visible within the same package).

Creating Struct Instances

// 1. Named field literal (recommended)
p := Person{
    Name:     "Ana",
    LastName: "Garcia",
    Age:      28,
    Email:    "[email protected]",
}

// 2. Positional fields (fragile, avoid)
p2 := Person{"Luis", "Perez", 35, "[email protected]"}

// 3. Zero value — all fields initialized to their zero value
var p3 Person  // Name:"", LastName:"", Age:0, Email:""

// 4. Pointer to struct
pp := &Person{Name: "Maria", Age: 22}
pp.Email = "[email protected]"  // Go dereferences automatically

Constructors: Factory Functions

Go has no special constructor syntax. The idiomatic pattern is to create New... functions that validate, initialize, and return the struct:

type Rectangle struct {
    Width  float64
    Height float64
    Color  string
}

// Constructor with validation
func NewRectangle(width, height float64, color string) (*Rectangle, error) {
    if width <= 0 || height <= 0 {
        return nil, fmt.Errorf("invalid dimensions: width=%.2f, height=%.2f", width, height)
    }
    return &Rectangle{
        Width:  width,
        Height: height,
        Color:  color,
    }, nil
}

// Usage
r, err := NewRectangle(5, 3, "blue")
if err != nil {
    fmt.Println("Error:", err)
    return
}

Methods: Behavior Associated with Structs

A method is a function with a receiver — a special parameter that indicates which type the method belongs to:

// Syntax: func (receiver ReceiverType) MethodName(params) (returns)

type Rectangle struct {
    Width, Height float64
}

// Value receiver — receives a copy of the struct
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

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

// Pointer receiver — can modify the original struct
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 5, Height: 3}
    fmt.Println("Area:", rect.Area())           // 15
    fmt.Println("Perimeter:", rect.Perimeter()) // 16

    rect.Scale(2)  // modifies rect directly
    fmt.Println("New width:", rect.Width)   // 10
    fmt.Println("New height:", rect.Height) // 6
}

Value Receiver vs. Pointer Receiver

This distinction is fundamental:

type Counter struct {
    value int
}

// Value receiver: does NOT modify the original
func (c Counter) Current() int {
    return c.value
}

func (c Counter) Increment() Counter {
    c.value++
    return c  // returns a modified copy
}

// Pointer receiver: DOES modify the original
func (c *Counter) IncrementInPlace() {
    c.value++
}

func main() {
    c := Counter{value: 0}

    c.IncrementInPlace()  // c.value = 1
    c.IncrementInPlace()  // c.value = 2
    fmt.Println(c.Current())  // 2

    newC := c.Increment()  // c.value stays 2
    fmt.Println(newC.Current())  // 3
    fmt.Println(c.Current())     // 2 (unchanged)
}

Practical rule: If any method of a type needs a pointer receiver, make all methods of that type use a pointer receiver. Consistency makes interface implementation easier.

Embedding: Composition Over Inheritance

Go uses composition instead of inheritance. Embedding promotes the fields and methods of one struct into another:

type Animal struct {
    Name string
    Age  int
}

func (a Animal) Describe() string {
    return fmt.Sprintf("%s is %d years old", a.Name, a.Age)
}

type Dog struct {
    Animal        // embedding — Dog "inherits" Animal's fields and methods
    Breed  string
}

func (d Dog) Bark() string {
    return "Woof!"
}

func main() {
    rex := Dog{
        Animal: Animal{Name: "Rex", Age: 3},
        Breed:  "Labrador",
    }

    // Direct access to embedded fields and methods
    fmt.Println(rex.Name)       // "Rex" (promoted from Animal)
    fmt.Println(rex.Describe()) // "Rex is 3 years old" (promoted method)
    fmt.Println(rex.Bark())     // "Woof!"
    fmt.Println(rex.Breed)      // "Labrador"

    // You can also access explicitly
    fmt.Println(rex.Animal.Name)
}

Overriding Embedded Methods

func (d Dog) Describe() string {
    // Call the embedded method and add extra information
    return d.Animal.Describe() + fmt.Sprintf(", breed: %s", d.Breed)
}

Struct Tags: Metadata for Serialization

Struct tags are metadata added to fields using backticks. They are used by packages like encoding/json, encoding/xml, ORMs, and validation frameworks:

type User struct {
    ID        int    `json:"id" db:"user_id"`
    Name      string `json:"name" db:"name" validate:"required,min=2"`
    Email     string `json:"email" db:"email" validate:"required,email"`
    Password  string `json:"-"`  // omit from JSON
    CreatedAt time.Time `json:"created_at,omitempty"`
}

// JSON serialization
u := User{ID: 1, Name: "Ana", Email: "[email protected]"}
data, err := json.Marshal(u)
// {"id":1,"name":"Ana","email":"[email protected]"}

// JSON deserialization
var u2 User
json.Unmarshal([]byte(`{"id":2,"name":"Luis","email":"[email protected]"}`), &u2)

Common tags:

  • json:"name" — field name in JSON
  • json:"-" — omit field from JSON
  • json:",omitempty" — omit if zero value
  • db:"name" — column name in database
  • validate:"required" — validation with packages like go-playground/validator

Anonymous Structs

Useful for temporary data or tests:

// Anonymous struct in a variable
config := struct {
    Host  string
    Port  int
    Debug bool
}{
    Host:  "localhost",
    Port:  8080,
    Debug: true,
}

// Slice of anonymous structs (common in tests)
cases := []struct {
    input    int
    expected int
}{
    {1, 1},
    {5, 120},
    {10, 3628800},
}

Struct Comparison

Structs are comparable with == if all their fields are comparable (no slices, maps, or functions):

type Point struct{ X, Y int }

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

fmt.Println(p1 == p2)  // true
fmt.Println(p1 == p3)  // false

With structs and methods mastered, in the next lesson we will learn interfaces — the mechanism that makes polymorphism in Go elegant and implicit.

Value receiver vs. pointer receiver
Use a pointer receiver (*T) when the method needs to modify the struct, when the struct is large (avoids costly copies), or for consistency (if any method needs a pointer, make them all pointers). Use a value receiver (T) when the method only reads the struct and the struct is small.
Go uses composition, not inheritance
Go has no class inheritance. Instead it uses embedding (composition). When you embed a struct inside another, the inner struct's methods and fields are promoted to the outer struct. This achieves code reuse without the pitfalls of deep inheritance hierarchies.