Dev & EngARTICLE

Recursive function in Go to access values in nested maps

GO | I recently received a response from an API that contained a JSON with a complex, large, and nested structure. I needed to access a specific value

Recursive function in Go to access values in nested maps
Image: Cesar Gimenes

GO | I recently received a response from an API that contained a JSON with a complex, large, and nested structure. I needed to access a specific value inside that structure, but the path to that value could vary.

The idea, then, was to create a recursive function that would take a list with the path to the desired value and the data structure (a nested map) and return the corresponding value, if found.

I believe there are smarter ways to do this, but this recursive approach worked well for my case.

package main

import (
	"fmt"
)

func GetFromMap(parts []string, current any) (any, bool) {
	fmt.Printf("parts: %v, current: %v\n", parts, current)

	if len(parts) == 0 {
		return current, true
	}

	m, ok := current.(map[string]any)
	if !ok { // not a map, cannot descend
		return nil, false
	}

	key := parts[0]
	next, exists := m[key]
	if !exists {
		return nil, false
	}

	return GetFromMap(parts[1:], next)
}

func MustGetFromMap(path []string, data any) any {
	val, ok := GetFromMap(path, data)
	if !ok {
		panic(fmt.Sprintf("path %v not found in map", path))
	}
	return val
}

And here is an example of how you can test this function:

package main

import "testing"

func TestGetFromMap(t *testing.T) {
	data := map[string]any{
		"not_a_map": "just a string",
		"a": map[string]any{
			"b": map[string]any{
				"c": 42,
			},
			"d": "hello",
		},
		"x": 3.14,
	}

	tests := []struct {
		path          []string
		expectedVal   any
		expectedFound bool
	}{
		{
			path:          []string{"a", "b", "c"},
			expectedVal:   42,
			expectedFound: true,
		},
		{
			path:          []string{"a", "d"},
			expectedVal:   "hello",
			expectedFound: true,
		},
		{
			path:          []string{"x"},
			expectedVal:   3.14,
			expectedFound: true,
		},
		{
			path:          []string{"a", "not_found"},
			expectedVal:   nil,
			expectedFound: false,
		},
	}

	for _, tt := range tests {
		val, found := GetFromMap(tt.path, data)
		if found != tt.expectedFound {
			t.Errorf("GetFromMap(%v) found = %v; want %v", tt.path, found, tt.expectedFound)
		}
		if val != tt.expectedVal {
			t.Errorf("GetFromMap(%v) = %v; want %v", tt.path, val, tt.expectedVal)
		}
	}
}

And, please, when creating an API, remember your fellow programmer who will be consuming it. Simple, flat structures that don't change shape often are better. In fact, simple is always better.

Translated from the Brazilian Portuguese original · Read the original