Golang How to filter a Slice with Generics

eye-catch Golang
Sponsored links

How to add elements to a Slice

To add elements to the existing slice, append() function needs to be used. It requires at least two parameters. Slice variable and an element that we want to add to the slice. We can also add multiple elements at once.

var list []int

fmt.Println(list) // []
list = append(list, 9)
fmt.Println(list) // [9]
list = append(list, 8, 7, 6)
fmt.Println(list) // [9 8 7 6]
Sponsored links

Add elements in a funciton

Let’s try to add elements in a function. I defined the following two functions for comparison.

func addNineByValue(array []int) {
    // this value of array is never used (SA4006)go-staticcheck
    array = append(array, 9)
}
func addNineByPointer(array *[]int) {
    *array = append(*array, 9)
}

The parameter is a value or pointer. A warning is shown in the first function. Let’s see the result below.

fmt.Println(list) // [9 8 7 6]

addNineByValue(list)
fmt.Println(list) // [9 8 7 6]

addNineByPointer(&list)
fmt.Println(list) // [9 8 7 6 9]

The slice is not updated with the first function because it adds an element to copied slice. However, the second one updates the slice because it adds it to the original slice.

The second way is called a destructive function which changes the original data. This is a bad practice because the value is updated under the hood. It can cause a bug because the value is updated without knowing it.

The first way is the same as append() function. Of course, the value must be returned and the returned value must be assigned to the original variable. Stick to this way to avoid unnecessary bug!!

How to clear a Slice

In case we need to clear a slice, we can simply assign nil to the slice.

fmt.Println(list) // [9 8 7 6 9]
list = nil
fmt.Println(list) // []
fmt.Println(len(list)) // 0

Assigning nil perfectly works.

How to add the existing Slice to a Slice with three dots

The existing slice can be added to a slice with append() function. In this case, three dots need to be added to the slice variable. It expands the slice into a parameter list. It means that the elements are passed to the function.

mySlice := []int{9, 8, 7, 6}
list = append(list, mySlice...)
fmt.Println(list) // [9 8 7 6]

If the existing slice is empty or nil, no element is added. This is safe.

fmt.Println(list) // []
var emptyList []int
list = append(list, emptyList...)
fmt.Println(list) // []

How to add an array to a Slice

The three dots can’t be used for an array. See the following example.

myArray := [4]int{5, 4, 3, 2}
// cannot use myArray (variable of type [4]int) as []int value in argument to appendcompilerIncompatibleAssign
list = append(list, myArray...)

The array needs to be converted to a slice in this case. An array can be converted by using a colon.

myArray := [4]int{5, 4, 3, 2}
list = append(list, myArray[:]...)
fmt.Println(list) // [5 4 3 2]

fmt.Println(reflect.TypeOf(myArray).Kind())    // array
fmt.Println(reflect.TypeOf(myArray[:]).Kind()) // slice

The type of the original value is array but it was converted to slice with the colon.

What’s this colon? Let’s go dive into it in the next chapter.

Slice with colon to get ranged elements

By using a colon, we can easily extract ranged elements from a slice.

list = append(list, 1, 2, 3, 4)
fmt.Println(list)      // [1 2 3 4]
fmt.Println(list[:])   // [1 2 3 4]
fmt.Println(list[0:])  // [1 2 3 4]
fmt.Println(list[0:1]) // [1]
fmt.Println(list[0:2]) // [1 2]
fmt.Println(list[2:3]) // [3]
fmt.Println(list[2:2]) // []
fmt.Println(list[2:4]) // [3 4]
fmt.Println(list[:3])  // [1 2 3]

The number is index of the element. It extracts values between the start index and the end index but the value of the end index is not included.

The end index must be the same or bigger than the start index. Likewise, the index value must be 0 or positive value as you can see in the following example.

// invalid slice indices: 2 < 4
fmt.Println(list[4:2])
// invalid argument: index -3 (constant of type int) must not be negative
fmt.Println(list[:-3])

Utility functions for Slice

Golang doesn’t have useful utility functions because they can easily be implemented. Let’s define the functions here.

We need to know Generics to implement them. Check the following post if you are not familiar with Generics.

The slice has the following values in the following code.

fmt.Println(list) // [1 2 3 4]

How to filter Slice

We use any, a.k.a interface, to support all types. The filtering condition must be defined on the caller side and the function must return a boolean.

func Filter[T any](array []T, callback func(T) bool) []T {
    result := []T{}

    for _, value := range array {
        if callback(value) {
            result = append(result, value)
        }
    }

    return result
}

// [3 4]
fmt.Println(Filter(list, func(value int) bool { return value > 2 }))
// []
fmt.Println(Filter(list, func(value int) bool { return value > 4 }))

How to Select/Map data

If we need to do something with the elements and want to have the result in a variable, this function is useful. This is the same as map() in JavaScript/Python, Select() in C#.

It needs to have two types T and K. T is a slice type. K is returned value type. This is because int value might need to be converted to a string. To address these cases, a different type needs to be defined.

func Select[T any, K any](array []T, callback func(T) K) []K {
    result := make([]K, len(array))

    for index, value := range array {
        result[index] = callback(value)
    }

    return result
}

// [1 4 9 16]
fmt.Println(Select(list, func(value int) int { return value * value }))
// [result: 1 result: 4 result: 9 result: 16]
fmt.Println(Select(list, func(value int) string { return fmt.Sprintf("result: %d", value*value) }))

How to check if a Slice contains a value

It’s similar to Filter(). It returns a boolean if it finds an element.

func Some[T any](array []T, callback func(T) bool) bool {
    for _, value := range array {
        if callback(value) {
            return true
        }
    }

    return false
}

fmt.Println(Some(list, func(value int) bool { return value < 2 }))  // true
fmt.Println(Some(list, func(value int) bool { return value > 4 }))  // false
fmt.Println(Some(list, func(value int) bool { return value == 4 })) // true

If we want to put the value directly, we can implement it in the following way.

func Contains[T comparable](array []T, searchValue T) bool {
    for _, value := range array {
        if value == searchValue {
            return true
        }
    }

    return false
}

fmt.Println(Contains(list, 2)) // true
fmt.Println(Contains(list, 4)) // true
fmt.Println(Contains(list, 5)) // false

We can’t use any type for this function because some types aren’t comparable for example map.

How to extract the first element that matches a condition

To get the first element that matches the condition, we can implement it in the following way.

func Find[T any](array []T, callback func(T) bool) *T {
    for _, value := range array {
        if callback(value) {
            return &value
        }
    }

    return nil
}

fmt.Println(*Find(list, func(value int) bool { return value < 2 }))  // 1
fmt.Println(Find(list, func(value int) bool { return value > 4 }))   // <nil>
fmt.Println(*Find(list, func(value int) bool { return value == 4 })) // 4

But I think it’s better to follow Golang way. Let’s add the second return value to know whether it finds an element. It’s better to use copied value instead of a pointer.

func Find[T any](array []T, callback func(T) bool) (T, bool) {
    for _, value := range array {
        if callback(value) {
            return value, true
        }
    }

    var zeroValue T
    return zeroValue, false
}

value, ok := Find2(list, func(value int) bool { return value < 2 })
fmt.Printf("value: %v, ok: %t\n", value, ok) // value: 1, ok: true
value, ok = Find2(list, func(value int) bool { return value == 5 })
fmt.Printf("value: %v, ok: %t\n", value, ok) // value: 0, ok: false

zeroValue returns a default value of the type. In this example above, it’s 0 because it’s int type.

If the data type is map type, the result becomes as follows.

map1 := map[string]int{"first": 1, "second": 2}
map2 := map[string]int{"three": 3, "four": 4}
valueMap, ok := Find2([]map[string]int{map1, map2}, func(valueMap map[string]int) bool {
    _, ok := valueMap["first"]
    return ok
})
fmt.Printf("value: %v, ok: %t\n", valueMap, ok) // value: map[first:1 second:2], ok: true

valueMap, ok = Find2([]map[string]int{map1, map2}, func(valueMap map[string]int) bool {
    _, ok := valueMap["notExist"]
    return ok
})
fmt.Printf("value: %v, ok: %t\n", valueMap, ok) // value: map[], ok: false

Comments

Copied title and URL