Open-source project
darccio/mergo avatar
darccio/mergo

Mergo: merging Go structs and maps without the if-statements

Mergo: merging Go structs and maps since 2013

3,108 stars284 forksGoBSD-3-Clause

At a glance

What is it?
Mergo is a small Go library that fills zero-value fields in structs and maps, mostly to apply configuration defaults. It is stable, frozen at v1.0.2, and imports as dario.cat/mergo.
Who is it for?
Adopt Mergo if you need to apply defaults to a Go struct or map and want to avoid hand-written zero checks; it has been in production use since 2013 and the module is at v1.0.2. Do not adopt it if you need to merge unexported fields, or structs stored inside maps, since Go reflection makes those cases unreachable.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 36 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Mergo is for, and who actually needs it

Configuration in Go usually arrives in layers: hardcoded defaults, a config file, environment variables, maybe flags. Each layer should only set what it explicitly defines, and leave the rest alone. Without a merge step, that means a wall of conditionals checking whether a field is still its zero value before assigning. Mergo replaces that wall with one call.

The library merges same-type structs and same-type maps. Its rule is simple: a zero-value field in the destination gets the source's value. The README frames the purpose directly, calling it "a helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements."

The audience is Go developers writing config loaders, option structs, or any code where a base value must be filled in from a partial one. It is not a general-purpose deep-merge engine for arbitrary data, and it does not try to be. The README states the project is "stable and frozen, ready for production" and that "no new features are accepted" for v1, with ideas deferred to a future v2.

The zero-value rule and what reflection cannot reach

Mergo works through Go reflection, and that single fact explains nearly every limitation in the README. Reflection can only set exported fields, so unexported fields are skipped entirely. The README is explicit: "Mergo won't merge unexported (private) fields. It will do recursively any exported one."

The same constraint hits structs stored inside maps. Map values are not addressable through reflection, so a struct sitting as a map value cannot have its fields written to. The README says Mergo "won't merge structs inside maps (because they are not addressable using Go reflection)." Maps themselves do merge recursively, so the boundary is specifically struct values held in a map.

There is a third edge that surprises people. An empty struct is itself a zero value, so it will not be merged. The README notes it "won't merge empty structs value as they are zero values too." If your config type contains a nested struct that is intentionally empty, Mergo will treat it as unset.

By default the destination wins where it already holds a value. To flip that, you pass the transformer WithOverride, which the README describes as merging "overwriting values." Pointers have their own switch: by default Mergo dereferences, and the README says that to assign the source pointer itself to the destination pointer you must add WithoutDereference alongside WithOverride.

Installing Mergo and running a first merge

Installation is a single go get against the vanity module path. The README gives this exact command, followed by the import line to use in your Go code.

bash
go get dario.cat/mergo
go
import (
    "dario.cat/mergo"
)

Note the path: dario.cat/mergo, not the older github.com/imdario/mergo. The README states that in 1.0.0 Mergo "moves to a vanity URL dario.cat/mergo" and that "no more v1 versions will be released." The repository's go.mod declares the module as dario.cat/mergo with go 1.18, so your toolchain needs to be at least that.

A minimal merge takes a destination pointer and a source value. The README's first example is the whole API surface in one line:

go
if err := mergo.Merge(&dst, src); err != nil {
    // ...
}

After that call, every field in dst that was still at its zero value now holds the corresponding value from src. Fields dst had already set are untouched. If you want src to win instead, pass the transformer:

go
if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
    // ...
}

The README does not document rollback or an undo path, so treat the merge as a one-way write to the destination.

Pointers, dereferencing, and the WithoutDereference switch

Pointer fields are where the default behaviour stops being obvious. Mergo dereferences by default, meaning it follows the pointer and merges the pointed-to value rather than replacing the pointer itself. If you want the source pointer assigned directly to the destination pointer, you need an extra option.

The README's example builds two structs with a *string field A and an int64 field B, where the source and destination each point at a different string. It then calls Merge with both WithOverride and WithoutDereference so that the source pointer's value is assigned to the destination's pointer. The README states the requirement plainly: "If you need to override pointers, so the source pointer's value is assigned to the destination's pointer, you must use WithoutDereference."

This is a real design trade-off, not a bug. Dereferencing is the safer default for nested config, because it merges the contents rather than swapping identity. But it means two code paths that look identical except for one option can produce different pointer graphs. If your config structs share pointers between layers, decide deliberately which behaviour you want and write a test that pins it.

The import path change is the biggest upgrade hazard

The move to dario.cat/mergo at v1.0.0 is the single most disruptive thing about adopting or upgrading this library, and it is not about API behaviour at all. Go modules identify a dependency by its path, so a project importing github.com/imdario/mergo and a project importing dario.cat/mergo are referring to different modules even though the code is the same lineage.

The README addresses one specific case: when the vanity URL causes trouble because Mergo is not a direct dependency of your project but is pulled in by something else. The recommended fix there is a replace directive pinning to the last release under the old import URL.

code
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16

Read that carefully. It pins to v0.3.16, the final old-path release, and it is offered for indirect dependencies only. If Mergo is a direct dependency in your code, the README's guidance is the go get against dario.cat/mergo shown earlier. The README also records a rough patch in the 0.3.x line: a problematic PR broke 0.3.9, it was reverted in 0.3.10, and the author describes 0.3.10 as "stable but not bug-free." If you are on 0.3.9, that note is worth reading before you plan anything else.

Mergo versus writing the merge yourself, or using a defaults library

The honest alternative for most Go teams is not another library. It is a hand-written merge function, or a defaults package built around explicit assignment. The difference in approach is control versus brevity.

A hand-written merge lets you decide, field by field, what counts as unset. A boolean field is the clearest case: Mergo treats false as a zero value, so it cannot distinguish "the caller set this to false" from "the caller said nothing." A hand-written merge can use a *bool or a separate presence flag and get that distinction right. Mergo's zero-value rule cannot, because false is false.

The trade-off runs the other way for large config structs with dozens of string, int and duration fields. Writing and maintaining per-field conditionals for that shape is tedious and easy to get wrong when a field is added. Mergo collapses it to one call and picks up new exported fields automatically. There is also the ecosystem argument: the README lists containerd, Docker CLI, Moby, Grafana Loki, GoReleaser and others among projects using it, which is evidence the zero-value approach holds up at scale even if it is not right for every field type.

If your config has several presence-sensitive booleans, Mergo is the wrong tool for those fields specifically. Mixing it with explicit handling for the ambiguous ones is a reasonable middle path.

Maintenance, licence, and what upgrading costs

The repository is not archived, and the last push was on 2026-08-24. That is recent, but it should not be read as active feature development, because the README states the opposite: Mergo is "stable and frozen" and "no new features are accepted." The recent activity is consistent with a frozen v1 receiving maintenance rather than new surface area.

Release cadence is slow by design. v1.0.0 landed on 2023-06-20, v1.0.1 on 2024-08-17, and v1.0.2 on 2025-05-07. Anyone expecting frequent releases should plan around the opposite. The practical upgrade cost is low for patch releases within v1, but the 1.0.0 path change means any project still on the github.com/imdario/mergo import has a migration step rather than a version bump.

The licence is BSD-3-Clause, which is permissive and generally compatible with both open source and proprietary Go code, but the repository's LICENSE file is the authority and this is not legal advice. One governance detail worth noting: the README mentions Tidelift and sponsorship channels, which signals a maintainer-funded project rather than a corporate-backed one. That matters for how you weigh the frozen-v1 policy if your roadmap depends on fixes for corner cases the README says are deferred to a future v2.

Editorial conclusion

Adopt Mergo if you need to apply defaults to a Go struct or map and want to avoid hand-written zero checks; it has been in production use since 2013 and the module is at v1.0.2. Do not adopt it if you need to merge unexported fields, or structs stored inside maps, since Go reflection makes those cases unreachable. Before you commit, check whether your dependency graph still pulls github.com/imdario/mergo, because the module path changed to dario.cat/mergo at v1.0.0 and the README's replace directive only applies when the old path is an indirect dependency.

Frequently asked questions

How do I install Mergo in a Go project?

Run go get dario.cat/mergo, then import "dario.cat/mergo" in your code. The repository's go.mod declares go 1.18, so your toolchain must be at least that version.

How do I use Mergo to merge two structs?

Call mergo.Merge(&dst, src) with a pointer to the destination. Fields in dst that are still at their zero value receive the corresponding value from src, and fields dst already set are left alone.

What does Mergo do with unexported struct fields?

It skips them. The README states that Mergo won't merge unexported (private) fields, though it will recurse into any exported one, because Go reflection cannot set unexported fields.

Why is my struct inside a map not being merged by Mergo?

Structs stored as map values are not addressable through Go reflection, so Mergo cannot write to their fields. The README says maps merge recursively except for structs inside maps for exactly this reason.

What is the import path for Mergo, dario.cat/mergo or github.com/imdario/mergo?

The current path is dario.cat/mergo, which Mergo moved to in v1.0.0, and the README states no more v1 versions will be released. The old github.com/imdario/mergo path stops at v0.3.16, and the README offers a replace directive for projects where it is only an indirect dependency.

How do I make Mergo overwrite existing values in the destination?

Pass the WithOverride transformer to Merge, as in mergo.Merge(&dst, src, mergo.WithOverride). If you also need the source pointer assigned directly to the destination pointer rather than dereferenced, add WithoutDereference.

Official sources

  1. darccio/mergo on GitHub
  2. Issues
  3. License: BSD-3-Clause
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/darccio-mergo.svg)](https://hysenlabs.com/projects/darccio-mergo)