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 {
// ...
}
iis the index of the current element.valueis 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.”
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:
attrscontains twoAttributestructs.- The first iteration sets
ito0. attrs[0].Keyis"id", so theifbody is skipped.- The second iteration sets
ito1. attrs[1].Keyis"href", so the condition is true.attrs[1].Val = "/dashboard"writes directly to the second struct in the slice.- After the loop, the slice contains
{Key: "href", Val: "/dashboard"}at index1.
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
}
}
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
| Pattern | Does it modify the slice element? | Best use |
|---|---|---|
for _, attr := range attrs | No | Read-only access to element values |
for i, attr := range attrs { attrs[i]... } | Yes | Read the copied value and update by index |
for i := range attrs { attrs[i]... } | Yes | Simple in-place updates; avoids an unnecessary value copy |
for i := range attrs { attr := &attrs[i] } | Yes | Multiple updates through a readable local pointer |
for _, attr := range attrs { p := &attr } | No | Points 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.
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
rangevariable is a value copy for a slice of structs. &attrs[i]points to the real slice element.&attrinsidefor _, attr := range attrsdoes not point toattrs[i].- Prefer
for i := range attrswhen you need to modify elements. - Use
breakorreturnwhen 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.
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
- Define the
AttributeandNodetypes. - Create a
SetAttributemethod on*Node. - Update the value when an attribute with the requested key already exists.
- Add a new attribute when no matching key exists.
- Demonstrate updating an
hrefattribute and adding atitleattribute.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.