How to search for an element in a golang slice
Asked Answered
D

9

150

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"?

Decade answered 29/7, 2016 at 8:46 Comment(1)
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 postVrablik
M
287

Starting with Go 1.21, there's the slices package in the standard lib with a generic search function named slices.IndexFunc():

func IndexFunc[S ~[]E, E any](s S, 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.

For older versions read on.

Before Go 1.21, it was part of an external repository: golang.org/x/exp/slices package which contains a generic "find" function named slices.IndexFunc():

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
}
Misdate answered 29/7, 2016 at 8:49 Comment(8)
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?Decade
@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.Misdate
ok I created var myconfig []*Config and used your 1st approach. I hope this is the best option performance wiseDecade
@love2code Best option performance wise is to build a map from it which you can index. See edited answer.Misdate
Note that golang.org/x/exp/slices is experimental. I wouldn't recommend using it at this stage.Remnant
Is there a go to consensus of which look up is "faster" and "efficient"? Given the examples replicated by @icza?Seismic
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!Barracks
@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).Misdate
C
35

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

Chalcocite answered 29/3, 2018 at 15:38 Comment(1)
You should check whether returned index out of range first, before compare.Eustoliaeutectic
U
11

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)
}   
Utham answered 29/7, 2016 at 8:51 Comment(0)
G
7

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)
        }
    }

}
Gotham answered 29/7, 2016 at 8:53 Comment(0)
F
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
}
Figural answered 28/4, 2022 at 4:29 Comment(0)
M
2

As other guyspeople 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)    
    }
}
Mannuela answered 7/10, 2020 at 8:54 Comment(0)
R
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")
    }
}
Rosanne answered 28/9, 2021 at 19:12 Comment(0)
Y
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

Yabber answered 23/1, 2023 at 11:50 Comment(0)
D
0

If you are looking for a function like Array.prototype.find() in Javascript, create this function:

func array_find[T any](a []T, fn func(u *T) bool) *T {
    for k, v := range a {
        if fn(&v) {
            return &a[k]
        }
    }
    var empty T
    return &empty
}

And it is easy to use:

tom := array_find(users, func(u *User) bool {
        return u.name == "Tom"
    })

Just like Array.prototype.find(), you get the reference of that object. Means, if you change values of tom, the values of original users also change.

Desquamate answered 25/1 at 19:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.