142

I have a slice of structs.

type Config struct {
    Key string
    Value string
}

// I form a slice of the above struct
var myconfig []Config 

// unmarshal a response body into the above slice
if err := json.Unmarshal(respbody, &myconfig); err != nil {
    panic(err)
}

fmt.Println(config)

Here is the output of this:

[{key1 test} {web/key1 test2}]

How can I search this array to get the element where key="key1"?

icza
  • 389,944
  • 63
  • 907
  • 827
codec
  • 7,978
  • 26
  • 71
  • 127
  • As your Config struct looks like a simple map I want to point out that you can decode any JSON data to a `map[string]interface{}`. If you're interested, checkout [this official blog post](https://blog.golang.org/json-and-go) – tsabsch Jul 29 '16 at 09:09

8 Answers8

267

Starting with Go 1.18 which adds generics support, there's a golang.org/x/exp/slices package which contains a generic "find" function named slices.IndexFunc():

func IndexFunc[E any](s []E, f func(E) bool) int

IndexFunc returns the first index i satisfying f(s[i]), or -1 if none do.

Using that:

idx := slices.IndexFunc(myconfig, func(c Config) bool { return c.Key == "key1" })

Try it on the Go Playground.

Prior to Go 1.18 and for a faster alternative, read on:

With a simple for loop:

for _, v := range myconfig {
    if v.Key == "key1" {
        // Found!
    }
}

Note that since element type of the slice is a struct (not a pointer), this may be inefficient if the struct type is "big" as the loop will copy each visited element into the loop variable.

It would be faster to use a range loop just on the index, this avoids copying the elements:

for i := range myconfig {
    if myconfig[i].Key == "key1" {
        // Found!
    }
}

Notes:

It depends on your case whether multiple configs may exist with the same key, but if not, you should break out of the loop if a match is found (to avoid searching for others).

for i := range myconfig {
    if myconfig[i].Key == "key1" {
        // Found!
        break
    }
}

Also if this is a frequent operation, you should consider building a map from it which you can simply index, e.g.

// Build a config map:
confMap := map[string]string{}
for _, v := range myconfig {
    confMap[v.Key] = v.Value
}

// And then to find values by key:
if v, ok := confMap["key1"]; ok {
    // Found
}
icza
  • 389,944
  • 63
  • 907
  • 827
  • Thanks I am free to use pointers as well. Do you think using a pointer to struct would be faster here? Can we have an array of pointers? – codec Jul 29 '16 at 08:52
  • @love2code Yes, you may use slice of pointers too, or if you loop over the indices (as in my 2nd example), then values will not be copied. Up to you. – icza Jul 29 '16 at 08:54
  • ok I created `var myconfig []*Config` and used your 1st approach. I hope this is the best option performance wise – codec Jul 29 '16 at 09:02
  • 2
    @love2code Best option performance wise is to build a map from it which you can index. See edited answer. – icza Jul 29 '16 at 09:03
  • Note that golang.org/x/exp/slices is experimental. I wouldn't recommend using it at this stage. – Tomer Apr 21 '22 at 15:20
  • Is there a go to consensus of which look up is "faster" and "efficient"? Given the examples replicated by @icza? – andres Jul 16 '22 at 19:06
  • Creating a map is not more efficient (memory-wise) since it requires making a new map, which uses memory. It is also not always faster. For infrequent checks in a small slice, it will take longer to make the new map than to simply traverse the slice to check. If # of checks is m, then naive loop through the slice: O(m*n) vs make map then check: O(n) to make map + O(m) to check if an item is in the map. Most of the time, for a reasonable size slice and a reasonable number of checks, then making a map makes sense. But not always! – Tom Anderson Jan 18 '23 at 02:09
  • @TomAnderson Obviously the map solution is to be used if the slice doesn't change (or not often), and you build the map once, and use it for multiple checks. If you only need to check once, obviously building a map is an unnecessary overhead as that requires iterating over all element once, while searching for an element might not even require iterating over the whole slice (not to mention building a map requires hashing and other calculations). – icza Jan 18 '23 at 08:38
33

You can use sort.Slice() plus sort.Search()

type Person struct {
    Name string
}

func main() {
    crowd := []Person{{"Zoey"}, {"Anna"}, {"Benni"}, {"Chris"}}

    sort.Slice(crowd, func(i, j int) bool {
        return crowd[i].Name <= crowd[j].Name
    })

    needle := "Benni"
    idx := sort.Search(len(crowd), func(i int) bool {
        return string(crowd[i].Name) >= needle
    })

    if idx < len(crowd) && crowd[idx].Name == needle {
        fmt.Println("Found:", idx, crowd[idx])
    } else {
        fmt.Println("Found noting: ", idx)
    }
}

See: https://play.golang.org/p/47OPrjKb0g_c

Tarion
  • 16,283
  • 13
  • 71
  • 107
10

You can save the struct into a map by matching the struct Key and Value components to their fictive key and value parts on the map:

mapConfig := map[string]string{}
for _, v := range myconfig {
   mapConfig[v.Key] = v.Value
}

Then using the golang comma ok idiom you can test for the key presence:

if v, ok := mapConfig["key1"]; ok {
    fmt.Printf("%s exists", v)
}   
icza
  • 389,944
  • 63
  • 907
  • 827
Endre Simo
  • 11,330
  • 2
  • 40
  • 49
6

There is no library function for that. You have to code by your own.

for _, value := range myconfig {
    if value.Key == "key1" {
        // logic
    }
}

Working code: https://play.golang.org/p/IJIhYWROP_

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    type Config struct {
        Key   string
        Value string
    }

    var respbody = []byte(`[
        {"Key":"Key1", "Value":"Value1"},
        {"Key":"Key2", "Value":"Value2"}
    ]`)

    var myconfig []Config

    err := json.Unmarshal(respbody, &myconfig)
    if err != nil {
        fmt.Println("error:", err)
    }

    fmt.Printf("%+v\n", myconfig)

    for _, v := range myconfig {
        if v.Key == "Key1" {
            fmt.Println("Value: ", v.Value)
        }
    }

}
Sadegh
  • 13
  • 5
Pravin Mishra
  • 8,298
  • 4
  • 36
  • 49
3

If anyone is coming from Java or C# like me this is what I ended up doing:

type Person struct {
    Name string
    Age  int
}
// create slice of people
var people []Person = []Person{
    {"Tono", 33},
    {"Regina", 25},
    {"Bob", 40},
}

// find person where its Name equals to Bob <------------------
bob := FirstOrDefault(people, func(p *Person) bool { return p.Name == "Bob" })

if bob != nil {
    fmt.Printf("Found bob: %v \n", *bob)
} else {
    fmt.Println("could not find bob")
}

peopleOlderThan30 := Where(people, func(p *Person) bool { return p.Age > 30 })

fmt.Println("People older than 30 are:")
for _, element := range peopleOlderThan30 {
    fmt.Println(*element)
}

I was able to run that code with the help of these functions:

func FirstOrDefault[T any](slice []T, filter func(*T) bool) (element *T) {

    for i := 0; i < len(slice); i++ {
        if filter(&slice[i]) {
            return &slice[i]
        }
    }

    return nil
}

func Where[T any](slice []T, filter func(*T) bool) []*T {

    var ret []*T = make([]*T, 0)

    for i := 0; i < len(slice); i++ {
        if filter(&slice[i]) {
            ret = append(ret, &slice[i])
        }
    }

    return ret
}
Tono Nam
  • 34,064
  • 78
  • 298
  • 470
1

As other people commented before, you can write your own procedure with an anonymous function to solve this issue.

I used two ways to solve it:

func Find(slice interface{}, f func(value interface{}) bool) int {
    s := reflect.ValueOf(slice)
    if s.Kind() == reflect.Slice {
        for index := 0; index < s.Len(); index++ {
            if f(s.Index(index).Interface()) {
                return index
            }
        }
    }
    return -1
}

Uses example:

type UserInfo struct {
    UserId          int
}

func main() {
    var (
        destinationList []UserInfo
        userId      int = 123
    )
    
    destinationList = append(destinationList, UserInfo { 
        UserId          : 23,
    }) 
    destinationList = append(destinationList, UserInfo { 
        UserId          : 12,
    }) 
    
    idx := Find(destinationList, func(value interface{}) bool {
        return value.(UserInfo).UserId == userId
    })
    
    if idx < 0 {
        fmt.Println("not found")
    } else {
        fmt.Println(idx)    
    }
}

Second method with less computational cost:

func Search(length int, f func(index int) bool) int {
    for index := 0; index < length; index++ {
        if f(index) {
            return index
        }
    }
    return -1
}

Uses example:

type UserInfo struct {
    UserId          int
}

func main() {
    var (
        destinationList []UserInfo
        userId      int = 123
    )
    
    destinationList = append(destinationList, UserInfo { 
        UserId          : 23,
    }) 
    destinationList = append(destinationList, UserInfo { 
        UserId          : 123,
    }) 
    
    idx := Search(len(destinationList), func(index int) bool {
        return destinationList[index].UserId == userId
    })
    
    if  idx < 0 {
        fmt.Println("not found")
    } else {
        fmt.Println(idx)    
    }
}
Jon Bright
  • 13,388
  • 3
  • 31
  • 46
omotto
  • 1,721
  • 19
  • 20
1

Here is a simple function based on @Tarion's answer.

func findProgram (programs []Program, id uint) (Program, error) {
    sort.Slice(programs, func(i, j int) bool {
        return programs[i].ID <= programs[j].ID
    })

    idx := sort.Search(len(programs), func(i int) bool {
        return programs[i].ID >= id
    })

    if idx < len(programs) && programs[idx].ID == id {
        return programs[idx], nil
    } else {
        return Program{}, fmt.Errorf("program not found")
    }
}
Nate Bunney
  • 2,418
  • 2
  • 22
  • 31
0

Checks for every element in the slice, including substrings. Case insensitive.

package main

import (
    "fmt"
    "strings"
)

func findSubstring(sliceStrings []string, substring string) []string {
    substring = strings.ToLower(substring)
    var matches []string
    for _, v := range sliceStrings {
        if strings.Contains(strings.ToLower(v), substring) {
            matches = append(matches, v)
        }
    }
    return matches
}

func main() {
    sliceStrings := []string{"Hello", "World", "world2", "golang"}
    substring := "oRl"
    matches := findSubstring(sliceStrings, substring)
    for _, value := range matches {
        fmt.Println(value)
    }
}

https://go.dev/play/p/m5UAgbvPbbR

Vityata
  • 42,633
  • 8
  • 55
  • 100