The compiler stops you at the loop
You are building a service that aggregates data from multiple sources. You define a custom struct to hold the results, complete with metadata and a slice of items. You fetch the data, populate the struct, and write a loop to process the items. The compiler rejects the code immediately with cannot range over result (type Result).
You stare at the code. The data is right there inside the struct. The slice is populated. The logic is sound. Go refuses to iterate. This error happens because range is strict about types. It does not inspect the contents of a value. It checks the type tag and only proceeds if the type matches a specific set of built-in categories.
What range actually accepts
The range keyword is a loop construct that generates an iterator. It knows how to walk over five specific shapes: slices, arrays, maps, strings, and channels. If the expression after range is not one of these types, the compiler aborts.
Think of range like a conveyor belt sorter in a factory. The sorter has slots for boxes, crates, bins, and flat sheets. If you hand it a welded metal casing, the sorter doesn't try to pry it open. It doesn't guess that there might be boxes inside. It stops the line and flags the error. Go works the same way. A struct is a container, not a collection. A pointer is an address, not a sequence. The compiler requires you to point to the actual collection inside the container.
range is strict. It won't guess your intent. Give it a shape it recognizes.
Minimal example: unwrapping the type
The fix is almost always to access the underlying iterable field. If your value is a struct, range over the slice field. If your value is a pointer, dereference it.
package main
import "fmt"
// ItemList wraps a slice with extra metadata.
type ItemList struct {
Name string
Items []string
}
func main() {
list := ItemList{
Name: "Groceries",
Items: []string{"apples", "bread", "milk"},
}
// This fails. ItemList is a struct, not a slice.
// Compiler error: cannot range over list (type ItemList)
// for _, item := range list {
// fmt.Println(item)
// }
// Fix: range over the Items field, which is a slice.
// Access the field explicitly to satisfy the type checker.
for _, item := range list.Items {
fmt.Println(item)
}
}
The compiler checks the type of list.Items. It sees []string. That matches the slice rule. The loop compiles. The key insight is that Go does not perform implicit conversions. It does not look for a ToSlice() method. You must write the access path yourself.
The compiler is a type checker, not a mind reader. Explicit access beats implicit magic.
How the compiler decides
When the compiler encounters a range statement, it performs a static type check. It looks at the expression and queries the type system. If the type is []T, [N]T, map[K]V, string, or chan T, the compiler generates the loop machinery.
For a slice, the compiler emits code to read the length, initialize an index variable, and fetch elements by offset. For a map, it emits code to iterate over the hash table buckets, yielding keys and values. For a string, it decodes UTF-8 sequences to yield runes.
If the type is anything else, the compiler has no rule to apply. It does not search the type's methods. It does not check if the type embeds a slice. It reports an error. This design keeps the language predictable. You can look at a range loop and know exactly what is happening without tracing through method calls or hidden interfaces.
Go avoids hidden costs. Implicit iteration could mask performance issues or logic errors. By forcing you to name the collection, the language makes the data flow visible. If you want to iterate, you must show the compiler where the sequence lives.
Realistic example: handling API responses
In real code, this error often appears when working with API responses or domain models. You define a response struct to carry status codes and headers alongside the payload. You need to process the payload, but the loop targets the whole response.
package main
import (
"encoding/json"
"net/http"
)
// APIResponse wraps the data with HTTP metadata.
type APIResponse struct {
Status int
Code string
Data []string
}
// FetchHandler processes an incoming request.
// FetchHandler returns a JSON response with processed items.
func FetchHandler(w http.ResponseWriter, r *http.Request) {
// Simulate fetching data.
resp := APIResponse{
Status: 200,
Code: "OK",
Data: []string{"user1", "user2", "user3"},
}
// You cannot range over resp directly.
// resp is a struct containing a slice, not a slice itself.
// Compiler error: cannot range over resp (type APIResponse)
// Range over the Data field.
// This accesses the underlying slice that holds the strings.
for _, user := range resp.Data {
// Process each user.
// user is a copy of the string element.
_ = user
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
Wrap data in structs for structure, but unwrap it to iterate. Keep the shape clear.
Pitfalls: pointers, interfaces, and loop variables
The error message usually points to a type mismatch, but the root cause can be subtle. Pointers, interfaces, and loop variable semantics introduce common traps.
Pointers to slices
If you hold a pointer to a slice, range rejects it. A pointer is an address, not a collection. You must dereference the pointer to get the slice.
func process(ptr *[]string) {
// This fails. ptr is a pointer, not a slice.
// Compiler error: cannot range over ptr (type *[]string)
// for _, v := range ptr {
// _ = v
// }
// Dereference the pointer to access the slice.
// Check for nil first to avoid a runtime panic.
if ptr == nil {
return
}
for _, v := range *ptr {
_ = v
}
}
Dereferencing a nil pointer causes a panic. Always check for nil before dereferencing, or use a helper that handles the check.
Interface values
If a value has type interface{} or any, range rejects it. The compiler does not know the concrete type at compile time. You must assert the type to recover the iterable collection.
func processAny(val any) {
// This fails. val is an interface, not a slice.
// Compiler error: cannot range over val (type interface {})
// Assert the concrete type.
// Use the comma-ok idiom to handle the case where the assertion fails.
items, ok := val.([]string)
if !ok {
// Handle the error or return.
return
}
// Now items is a slice.
for _, v := range items {
_ = v
}
}
A type assertion that fails panics unless you use the comma-ok form. Prefer the comma-ok form in loops to avoid crashing on unexpected types.
Loop variable addresses
This is not a compile error, but it is a runtime bug that confuses beginners. The loop variable in a range statement is reused across iterations. If you take the address of the loop variable, you get the same pointer every time.
func collectAddresses(items []string) []*string {
var result []*string
for _, item := range items {
// item is a copy of the element.
// &item is the address of the loop variable, which is reused.
// This creates a slice of pointers to the same memory location.
// result = append(result, &item) // BUG
// Fix: create a local variable to get a unique address.
local := item
result = append(result, &local)
}
return result
}
The loop variable is a convention trap. Taking its address requires a local shadow. The worst bug is the one that looks correct but points to the wrong memory.
Decision: how to iterate safely
Choose the iteration strategy based on the type you hold. Use the right tool for the shape of your data.
Use range directly on a slice when the variable holds the collection and you need to iterate every element. Use range on a map when you need to process key-value pairs, remembering that iteration order is randomized. Use range on a string when you need to iterate over Unicode code points, not bytes. Use range on a channel when you need to receive values until the channel closes. Dereference a pointer with *ptr when the value is stored behind a pointer, but check for nil first to avoid a panic. Access the underlying slice field of a struct when the data is wrapped in a custom type. Perform a type assertion on an interface value when the concrete type is dynamic and you need to recover the iterable collection.