Question

Modifying Slice Elements While Ranging in Go

goarraysfor-loop

Question

Given these Go types:

type Attribute struct {
    Key string
    Val string
}

type Node struct {
    Attr []Attribute
}

How can I iterate over n.Attr and update the Val field for an attribute whose Key is "href"?

This does not update the slice element because attr is not a pointer:

for _, attr := range n.Attr {
    if attr.Key == "href" {
        attr.Val = "something"
    }
}

Using an index works:

for i, attr := range n.Attr {
    if attr.Key == "href" {
        n.Attr[i].Val = "something"
    }
}

Is there a simpler or faster approach? Can range directly provide pointers to the slice elements without changing the data structures?

Short Answer

In Go, the value variable produced by range is a copy of each slice element. Changing that copy does not change the original element. To update a slice in place, use the index to access the element, or take a pointer to the element with &slice[i].

Concept

A Go slice stores an ordered sequence of elements. When you range over a slice using two variables:

for i, value := range items {
    // ...
}
  • i is the index of the current element.
  • value is a copy of the current element.

For a slice of structs, assigning to a field on value only changes that local copy:

value.Val = "something" // Changes only value, not items[i].

This behavior matters because Go avoids silently giving every loop body direct mutable access through the value variable. It also means range works consistently across values such as structs, arrays, strings, and maps.

To modify the original slice element, access it through its index:

n.Attr[i].Val = "something"

Alternatively, create a pointer to the actual element at that index:

attr := &n.Attr[i]
attr.Val = "something"

Both approaches update the element stored in the slice.

Mental Model

Think of n.Attr as a row of labelled boxes. During this loop:

for _, attr := range n.Attr {
}

Go reads each box and hands you a photocopy named attr. Writing on the photocopy does not alter the original box.

The index i is the box number. n.Attr[i] means “open the original box at this position.” &n.Attr[i] means “keep its address so I can edit the original box through a pointer.”

Take Quiz

Syntax and Examples

Use an index-only range loop when the value variable is not needed:

for i := range n.Attr {
    if n.Attr[i].Key == "href" {
        n.Attr[i].Val = "something"
    }
}

This is often the clearest form. It does not make an unnecessary copy of each Attribute into a loop value variable.

If you prefer pointer-style field access, take the address of the element using its index:

for i := range n.Attr {
    attr := &n.Attr[i]

    if attr.Key == "href" {
        attr.Val = "something"
    }
}

A complete example:

package main

import "fmt"

type Attribute struct {
    Key string
    Val string
}

type Node struct {
    Attr []Attribute
}

func main() {
    n := Node{
        Attr: []Attribute{
            {Key: "class", Val: "button"},
            {Key: "href", Val: "/old-page"},
        },
    }

    for i := range n.Attr {
        if n.Attr[i].Key == "href" {
            n.Attr[i].Val = "/new-page"
        }
    }

    fmt.Println(n.Attr)
    // [{class button} {href /new-page}]
}

If only one href attribute should exist, add break after the update to stop scanning once it is found.

Step by Step Execution

Consider this code:

attrs := []Attribute{
    {Key: "id", Val: "header"},
    {Key: "href", Val: "/home"},
}

for i := range attrs {
    if attrs[i].Key == "href" {
        attrs[i].Val = "/dashboard"
    }
}

Execution trace:

  1. attrs contains two Attribute structs.
  2. The first iteration sets i to 0.
  3. attrs[0].Key is "id", so the if body is skipped.
  4. The second iteration sets i to 1.
  5. attrs[1].Key is "href", so the condition is true.
  6. attrs[1].Val = "/dashboard" writes directly to the second struct in the slice.
  7. After the loop, the slice contains {Key: "href", Val: "/dashboard"} at index 1.

By contrast, this loop updates only a copy:

for _, attr := range attrs {
    if attr.Key == "href" {
        attr.Val = "/dashboard"
    }
}

On the second iteration, attr starts as a copy of attrs[1]. Updating attr.Val does not write anything back to attrs[1].

Real World Use Cases

Updating slice elements in place is common when working with structured application data:

  • HTML or XML processing: change an href, src, or other attribute before rendering or saving a document.
  • API request normalization: update fields in a slice of request items after validation, such as trimming names or applying defaults.
  • Configuration processing: find a setting by key and replace its value.
  • Inventory updates: adjust the quantity or status of a product in a slice of structs.
  • Data cleanup scripts: mark records as processed, normalize categories, or redact selected fields.

Example: marking matching jobs as complete:

for i := range jobs {
    if jobs[i].ID == completedID {
        jobs[i].Status = "complete"
        break
    }
}
Take Quiz

Real Codebase Usage

In production Go code, developers usually choose one of these patterns.

Modify through an index

Best when the operation is short:

for i := range users {
    if users[i].ID == id {
        users[i].Active = false
        return nil
    }
}

This uses an early return after finding the target, which avoids unnecessary work and makes the successful path obvious.

Bind a pointer to the element

Useful when several fields are read or changed:

for i := range orders {
    order := &orders[i]
    if order.ID != orderID {
        continue
    }

    order.Status = "paid"
    order.PaidAt = now
    return nil
}

Validate before mutating

Real code often rejects invalid input before changing data:

if newURL == "" {
    return errors.New("URL cannot be empty")
}

for i := range n.Attr {
    if n.Attr[i].Key == "href" {
        n.Attr[i].Val = newURL
        return nil
    }
}

return errors.New("href attribute not found")

Use a helper when lookup is repeated

If many parts of a codebase need to find attributes, a helper can centralize the loop:

func findAttribute(attrs []Attribute, key string) *Attribute {
    for i := range attrs {
        if attrs[i].Key == key {
            return &attrs[i]
        }
    }
    return nil
}

The returned pointer refers to an element in the slice. Avoid retaining it if the slice may later be reallocated by operations such as append.

Common Mistakes

Changing the ranged value

This compiles but does not modify the slice:

for _, attr := range n.Attr {
    attr.Val = "something" // attr is a copy
}

Use n.Attr[i].Val or &n.Attr[i] instead.

Taking the address of the ranged value

This is not a pointer to the slice element:

for _, attr := range n.Attr {
    attrPtr := &attr // Pointer to the loop variable, not &n.Attr[i]
    attrPtr.Val = "something"
}

Even in Go 1.22 and later, where range variables declared with := are recreated per iteration, &attr still points to a copied loop value rather than the element in the slice. Use this reliable form:

for i := range n.Attr {
    attrPtr := &n.Attr[i]
    attrPtr.Val = "something"
}

Expecting a value loop to write back automatically

Go does not copy a changed attr back to the slice after the loop body. If you intentionally modify the copy, you must explicitly assign it back:

for i, attr := range n.Attr {
    if attr.Key == "href" {
        attr.Val = "something"
        n.Attr[i] = attr
    }
}

This works, but directly updating n.Attr[i].Val is usually simpler.

Forgetting to stop when only one match is expected

If only the first matching attribute should change, use break or return:

for i := range n.Attr {
    if n.Attr[i].Key == "href" {
        n.Attr[i].Val = "something"
        break
    }
}

Without it, every matching href attribute is updated.

Comparisons

PatternDoes it modify the slice element?Best use
for _, attr := range attrsNoRead-only access to element values
for i, attr := range attrs { attrs[i]... }YesRead the copied value and update by index
for i := range attrs { attrs[i]... }YesSimple in-place updates; avoids an unnecessary value copy
for i := range attrs { attr := &attrs[i] }YesMultiple updates through a readable local pointer
for _, attr := range attrs { p := &attr }NoPoints to the copied range value, not the slice element

A slice of pointers behaves differently:

attrs := []*Attribute{
    {Key: "href", Val: "/old"},
}

for _, attr := range attrs {
    attr.Val = "/new" // Works: attr is a copied pointer to the same Attribute
}

This works because copying a pointer still refers to the same underlying object. However, changing []Attribute to []*Attribute has ownership, allocation, and nil-handling implications, so it should be a data-model decision rather than a workaround for one loop.

Take Quiz

Cheat Sheet

// Read values only: value is a copy.
for _, attr := range attrs {
    fmt.Println(attr.Key)
}

// Modify the original struct in a slice.
for i := range attrs {
    attrs[i].Val = "new value"
}

// Use a pointer to the actual slice element.
for i := range attrs {
    attr := &attrs[i]
    attr.Val = "new value"
}

// Stop after the first match.
for i := range attrs {
    if attrs[i].Key == "href" {
        attrs[i].Val = "new value"
        break
    }
}

Key rules:

  • The second range variable is a value copy for a slice of structs.
  • &attrs[i] points to the real slice element.
  • &attr inside for _, attr := range attrs does not point to attrs[i].
  • Prefer for i := range attrs when you need to modify elements.
  • Use break or return when there should be only one match.

FAQ

Why does changing a Go range variable not change a slice?

The value variable from range is a copy of the slice element. Mutating the copy does not mutate the original element stored in the slice.

What is the idiomatic way to modify a struct in a Go slice?

Use an index-only range loop:

for i := range items {
    items[i].Field = value
}

Can Go range return pointers to slice elements directly?

No. range provides an index and a value. To get a pointer to an element, use &items[i].

Is for i := range slice faster than for i, value := range slice?

It can avoid copying each value into value, which is especially useful when elements are large structs. For small structs, clarity is usually more important than a tiny performance difference.

Is &value in a range loop safe for modifying the slice?

No. It points to the range value variable, not the original slice element. Use &slice[i].

Should I use a slice of pointers instead of a slice of structs?

Not solely to make mutation easier. Use pointers when shared identity, optional values, or avoiding large value copies are genuine model requirements. A slice of structs plus index-based mutation is often simple and efficient.

What if several attributes have the same key?

The loop updates all matches unless you use break or return after the first update.

Take Quiz

Mini Project

Description

Build a small helper that updates an HTML-like attribute stored in a Node. The project demonstrates how to find and modify the original struct inside a slice, rather than modifying a copied range value.

Goal

Implement SetAttribute so it updates an existing attribute or adds it when the key is absent.

Requirements

  1. Define the Attribute and Node types.
  2. Create a SetAttribute method on *Node.
  3. Update the value when an attribute with the requested key already exists.
  4. Add a new attribute when no matching key exists.
  5. Demonstrate updating an href attribute and adding a title attribute.
Take Quiz

Keep learning

Related questions

View more