Question

Are Go Maps Passed by Value or Reference?

Question

In Go, are maps passed to functions by value or by reference?

For example, is using a pointer to a map necessary, or is it unnecessary complexity?

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

Similarly, when returning a map from a function, should the function return a map value or a pointer to a map?

The goal is to avoid unnecessary copying of map data.

Short Answer

Go always passes function arguments by value. When you pass a map, Go copies the small map value—not every key and value stored in the map. Both the original map and the copied map value refer to the same underlying map data, so changes to entries are visible to the caller. In most cases, accept and return map[K]V directly; use *map[K]V only when a function must replace, initialize, or set the caller's map variable itself.

Concept

A Go map is a value, so assigning it or passing it as a function argument copies that value.

However, a map value is a small descriptor that gives access to map storage managed by the runtime. Copying the descriptor does not duplicate all map entries. As a result, two map variables created from the same map value access the same underlying data.

func addStatus(statuses map[string]string) {
    statuses["api"] = "healthy"
}

func main() {
    statuses := map[string]string{"database": "healthy"}
    addStatus(statuses)

    // statuses now contains both entries.
}

This is why maps are sometimes informally called “reference types.” That phrase is useful for describing shared mutations, but the precise Go rule is: all arguments are passed by value.

This distinction matters because changing a map entry is different from assigning a new map to the parameter:

func replaceMap(statuses map[string]string) {
    statuses = map[string]string{"new": "map"}
}

replaceMap changes only its local copy of the map value. The caller's variable still holds its original map value.

Mental Model

Think of a map value as a small library card that tells you where a shared collection of books is stored.

  • Passing a map to a function gives the function a copy of the library card.
  • Both cards point to the same collection, so adding or removing books affects what both people see.
  • If the function throws away its card and receives a card for a different library, the caller still has the original card.

A *map[K]V is like giving the function access to the caller's card holder. The function can then replace the caller's card, including changing it from nil to a newly created map.

Take Quiz

Syntax and Examples

Pass a map directly when the function should read, add, update, or delete entries.

package main

import "fmt"

func setTheme(settings map[string]string, theme string) {
    settings["theme"] = theme
}

func main() {
    settings := map[string]string{"language": "en"}

    setTheme(settings, "dark")
    fmt.Println(settings)
    // map[language:en theme:dark]
}

The settings parameter is passed by value, but its copied map value accesses the same underlying map data as the caller's settings.

Returning a map value is also the normal choice:

func defaultSettings() map[string]string {
    return map[string]string{
        "language": "en",
        "theme":    "light",
    }
}

Returning the map does not copy every entry just because it is returned. It returns a map value that provides access to the created map data.

Use a pointer to a map only when the function must modify the caller's map variable, rather than only its entries:

func ensureSettings(settings *map[string]string) {
    if *settings == nil {
        *settings = make(map[string]string)
    }
    (*settings)["theme"] = "light"
}

Calling it requires ensureSettings(&settings). This extra syntax is usually unnecessary unless replacing or initializing the caller's map is part of the function's job.

Step by Step Execution

Consider this example:

package main

import "fmt"

func update(scores map[string]int) {
    scores["Ava"] = 95
    scores = map[string]int{"Sam": 80}
    scores["Lee"] = 88
}

func main() {
    scores := map[string]int{"Ava": 70}
    update(scores)
    fmt.Println(scores)
}

Step by step:

  1. main creates one map containing "Ava": 70.
  2. update(scores) copies the map value into the parameter named scores. Both map values initially access the same map data.
  3. scores["Ava"] = 95 updates an entry in that shared map. The caller will observe this change.
  4. scores = map[string]int{"Sam": 80} assigns a new map value to the local parameter only. It does not change the variable in main.
  5. scores["Lee"] = 88 modifies the new local map, not the original one.
  6. main prints map[Ava:95].

The important rule: entry mutations are shared; rebinding the parameter is local.

Real World Use Cases

Maps are commonly passed as values when a function needs to work with shared lookup or configuration data.

  • HTTP headers: Add or normalize header values in a map[string]string or http.Header.
  • Configuration: Fill in missing settings in a configuration map.
  • Counters: Increment counts while processing log lines, events, or records.
  • Validation errors: Add field-specific messages to a map[string]string or map[string][]string.
  • Caching: Store values in a shared in-memory cache map, with appropriate synchronization when concurrent.

Example: collecting word counts.

func countWords(words []string, counts map[string]int) {
    for _, word := range words {
        counts[word]++
    }
}

counts := make(map[string]int)
countWords([]string{"go", "map", "go"}, counts)
// counts is map[go:2 map:1]

No pointer to counts is needed because incrementing an entry changes the shared map data.

Take Quiz

Real Codebase Usage

In production Go code, function signatures usually accept and return map values directly:

func MergeLabels(base, extra map[string]string) map[string]string
func Validate(input map[string]string) map[string]string
func ApplyDefaults(config map[string]string)

Common patterns include:

  • Mutation APIs: A function documents that it adds, changes, or removes entries in the supplied map.
  • Non-mutating APIs: A function creates a new map when callers should retain the original unchanged.
  • Nil checks: A function that writes to a map checks whether it is nil when nil input is allowed.
  • Returning errors: Functions often return (map[K]V, error) rather than *map[K]V.
func parseLabels(input []string) (map[string]string, error) {
    labels := make(map[string]string)

    for _, item := range input {
        // Parse and validate item, then store it in labels.
    }

    return labels, nil
}

A pointer to a map can be reasonable when initialization or replacement is intentionally an in-place operation, but it is less common. Often, a clearer alternative is to return the new map:

func withDefaults(config map[string]string) map[string]string {
    if config == nil {
        config = make(map[string]string)
    }
    if _, ok := config["port"]; !ok {
        config["port"] = "8080"
    }
    return config
}

For concurrent access, maps need protection with a mutex or another suitable design. Passing a map by value does not make concurrent reads and writes safe.

Common Mistakes

Expecting reassignment to affect the caller

This does not replace the caller's map:

func reset(data map[string]int) {
    data = make(map[string]int) // local reassignment only
    data["count"] = 0
}

Return the replacement map instead, or use *map[string]int if modifying the caller's variable is genuinely required.

Writing to a nil map

A nil map can be read from and ranged over, but it cannot receive new entries.

var data map[string]int
data["count"] = 1 // panic: assignment to entry in nil map

Initialize it before writing:

data := make(map[string]int)
data["count"] = 1

Using a pointer just to avoid copying entries

func add(data *map[string]int) {
    (*data)["count"]++
}

This works for a non-nil map, but it adds dereferencing and makes calls less convenient. It does not provide a necessary performance benefit for ordinary map passing. Prefer func add(data map[string]int).

Accidentally modifying input

Because map entry changes are shared, a helper can unexpectedly alter its caller's data. If the function should not mutate the input, copy entries into a new map first.

func copyMap(source map[string]string) map[string]string {
    result := make(map[string]string, len(source))
    for key, value := range source {
        result[key] = value
    }
    return result
}

This is a shallow copy: if map values contain pointers, slices, or other reference-like data, those nested values may still be shared.

Ignoring concurrent access

A normal Go map is not safe for simultaneous reads and writes from multiple goroutines. Protect it with synchronization, or redesign ownership so one goroutine manages the map.

Comparisons

OperationAre map entries copied?Can entry changes be seen through the other value?Can it replace the caller's map variable?
b := aNoYesNo
func f(m map[K]V)NoYesNo
func f(m *map[K]V)NoYesYes, by assigning *m
Return map[K]VNo full entry copy merely from returningThe returned map accesses its own created or chosen map dataThe caller assigns the returned value
ChoiceBest useTrade-off
map[K]V parameterRead, add, update, or delete entriesCannot rebind the caller's variable
*map[K]V parameterInitialize, replace, or nil out the caller's map variableMore syntax and usually unnecessary
Return map[K]VConstruct, replace, or transform a mapCaller must assign the result

A map differs from an array. Passing an array copies every array element because an array value contains its elements directly. A slice, like a map, is a small value that can share underlying data, though slice operations have different rules around length and capacity.

Take Quiz

Cheat Sheet

// Normal: pass a map value.
func add(m map[string]int) {
    m["item"]++
}

// Normal: return a map value.
func build() map[string]int {
    return map[string]int{"item": 1}
}

// Use a map pointer only when replacing/initializing caller's variable.
func ensure(m *map[string]int) {
    if *m == nil {
        *m = make(map[string]int)
    }
}
  • Go passes all arguments by value.
  • Passing or assigning a map copies a small map value, not all entries.
  • Map copies access the same underlying map data.
  • m[key] = value and delete(m, key) affect shared map data.
  • m = otherMap changes only the local map variable or parameter.
  • A nil map is safe to read, range over, and delete from; writing to it panics.
  • Use make(map[K]V) or a map literal before writing.
  • A regular map is not safe for concurrent read/write access.
  • Copy entries manually when you need an independent map.

FAQ

Are maps pass-by-reference in Go?

No. Go passes all function arguments by value. A copied map value still accesses the same underlying map data, which is why entry modifications are visible to the caller.

Does passing a map to a Go function copy the entire map?

No. Passing a map copies the map value, not every key-value entry. You do not need a pointer to a map simply to avoid a full data copy.

Can a function modify a map passed as an argument?

Yes. It can add, update, and delete entries, and the caller sees those changes because both map values access the same underlying data.

Why does assigning a new map inside a function not affect the caller?

The function has its own copied map variable. Assigning that local variable changes only the local copy. It does not assign a new value to the caller's variable.

When should I use a pointer to a map in Go?

Use *map[K]V when the function must initialize a nil map, replace the map entirely, or otherwise assign to the caller's map variable. This is uncommon compared with passing a map directly.

Should a Go function return map[K]V or *map[K]V?

Usually return map[K]V. It is idiomatic, avoids extra dereferencing, and does not imply that all map entries are copied.

How do I make an independent copy of a map?

Create a new map and copy entries with a for loop. This creates a shallow copy; nested reference-like values can still be shared.

Is a map safe to share between goroutines?

Not without synchronization when there are writes. Use a mutex, channels, or another concurrency-safe design.

Take Quiz

Mini Project

Description

Build a small configuration helper for a command-line tool. The helper should add defaults to a supplied map without requiring callers to use a pointer. This demonstrates that map entry changes made inside a function are visible to the caller.

Goal

Create and print a configuration map containing user-provided values plus any missing defaults.

Requirements

  1. Create a map[string]string with one user-provided setting.
  2. Write an applyDefaults function that accepts a map value.
  3. Add default values only when a key is missing.
  4. Call the function and print the resulting map.
  5. Do not use a pointer to a map.
Take Quiz

Keep learning

Related questions

View more