Go

Are maps passed by value or by reference in Go

27 September 2026 · 6 min read

Are maps passed by value or by reference in Go

Understanding how data structures are handled in Go functions is fundamental for any developer aiming to write efficient and predictable code. A common point of confusion arises with Go maps: are maps passed by value or by reference in Go? This question delves into the core of Go’s memory model and how its built-in types behave when passed as arguments to functions. While many languages might distinguish explicitly between “pass by value” and “pass by reference,” Go adopts a consistent approach that, for maps, often leads to behavior similar to what one might expect from a “reference” type, even though it’s technically still pass-by-value. We’ll explore the nuances of this behavior, dissecting how Go maps are structured internally and what implications this has for modifications within functions, concurrency, and overall program design.

The Go Model: Everything is Passed by Value

In Go, all arguments to functions are passed by value. This means that when you pass a variable to a function, a copy of that variable is made and given to the function. For primitive types like integers, booleans, or strings, this is straightforward: the function receives a completely independent copy, and any modifications to this copy inside the function do not affect the original variable in the calling scope.

However, the crucial distinction lies in what “value” means for different data types. For complex types like slices, channels, and maps, their “value” is not the entire underlying data structure, but rather a small header struct that contains a pointer to the actual data. When a map is passed to a function, a copy of this header struct is made. Both the original map variable and the copied map variable in the function scope now hold pointers that point to the same underlying map data structure in memory. This is why modifications made through the copied map variable within the function will indeed be reflected in the original map outside the function.

This design choice provides efficiency. Copying large data structures like maps, which can grow significantly, would be computationally expensive. By passing a small header containing a pointer, Go avoids unnecessary data duplication while maintaining its “pass by value” semantic consistency. This behavior is critical for understanding data flow and avoiding unexpected side effects in your Go applications. For a deeper dive into Go’s memory management, the official Effective Go documentation on pointers offers valuable insights into these fundamental concepts.

Deconstructing Go Maps: The Header and Underlying Data

To truly grasp how maps are passed in Go, it’s essential to understand their internal structure. A Go map variable is not the map data itself, but rather a small, fixed-size header struct. This header contains several fields, most importantly a pointer to a runtime map struct (runtime.hmap) which holds the actual key-value pairs, hash buckets, and other metadata. This runtime.hmap is allocated on the heap.

When you declare a map, e.g., myMap := make(map[string]int), myMap becomes a variable that holds this header struct. When myMap is then passed to a function, a copy of this header struct is made. Both the original myMap and the function’s parameter variable now contain pointers that reference the identical runtime.hmap on the heap. Therefore, any operation performed on the map inside the function—like adding new key-value pairs, updating existing values, or deleting entries—directly modifies the shared underlying data structure, making these changes visible outside the function.

It’s this shared pointer that gives maps their “reference-like” behavior. If you were to reassign the map parameter within the function to a completely new map (e.g., func(m map[string]int) { m = make(map[string]int) }), only the local copy of the header struct would be updated to point to a new underlying map. The original map variable outside the function would remain unchanged, still pointing to its original data. This distinction is key to understanding why modifications to the contents of a map are persistent, but reassignment of the map variable itself is not.

Infographic: Go Map Behavior - Illustrates a map variable (header) pointing to an underlying runtime.hmap on the heap, and how passing it to a function creates a copy of the header, both pointing to the same hmap.
Practical Implications of Map Passing Behavior ----------------------------------------------

Understanding that maps are effectively shared when passed to functions has significant practical implications for your Go programs. It means you must be mindful of side effects: a function that receives a map can alter its contents, which will be visible to all other parts of your program that hold a reference to the same map. This can be a powerful feature for sharing state, but it also introduces potential pitfalls, especially in concurrent environments.

Consider a scenario where multiple goroutines access and modify the same map concurrently. Without proper synchronization mechanisms, this will lead to race conditions, as Go maps are not inherently safe for concurrent writes. The Go runtime will detect and panic if multiple goroutines attempt to write to a map without external locking. For concurrency-safe map operations, Go provides the sync.Map type or recommends using a sync.RWMutex to protect access to a standard map. This is a critical aspect of Go’s approach to concurrency, emphasizing explicit handling of shared mutable state.

Furthermore, this behavior influences how you might design APIs. If a function is intended to receive a map, process its data, and return a new map without affecting the original, the function must explicitly create and populate a new map. Conversely, if the intent is for the function to mutate the provided map, then no special handling is needed beyond ensuring thread safety if concurrent access is a possibility. This direct manipulation of the underlying data structure reinforces Go’s philosophy of explicit control and minimal magic.

When to Make a Copy of a Map

There are scenarios where you explicitly need to prevent a function from modifying the original map. In such cases, you must create a defensive copy of the map before passing it to the function. This ensures that the function operates on its own independent data set.

  1. Initialize a new map: Create an empty map of the same type as the original.
  2. Iterate and copy: Loop through the original map’s key-value pairs.
  3. Assign to new map: For each pair, add it to the newly created map.
  4. Pass the copy: Pass this new, independent map to your function.

This process ensures that the function receives a completely separate map, allowing it to perform any modifications without affecting the original data. This pattern is common in libraries or frameworks where input immutability is desired.

While all values in Go are passed by value, maps exhibit “reference-like” behavior because the value copied is a small header struct containing a pointer to the map’s Question & Answer :

Are maps passed by value or reference in Go ?

It is always possible to define a function as following, but is this an overkill ?

func foo(dat *map[string]interface{}) {...} 

Same question for return value. Should I return a pointer to the map, or return the map as value ?

The intention is of course to avoid unnecessary data copy.

In this thread you will find your answer :

Golang: Accessing a map using its reference

You don’t need to use a pointer with a map.

Map types are reference types, like pointers or slices[1]

If you needed to change the Session you could use a pointer:

map[string]*Session 

https://blog.golang.org/go-maps-in-action