Home > Software engineering >  How to sort an struct array by dynamic field name in golang
How to sort an struct array by dynamic field name in golang

Time:11-01

I want to sort a struct array by dynamic field. Here is the struct

type user struct{
    Name string `json:"name"`
    Age int     `json:"age"`
    Status int  `json:"status "`
    Type  string  `json:"type"`
}

This is an array of struct

var UserArray []user

I have to sort this array by a given field that can be any field of user struct. but I will receive that sorting field from UI as a JSON tag. Like below

sort := agnutil.GetQueryParamString(<json tag>, "sort", 0, "name")

I have tried the sort function in golang but How to use that dynamically??

sort.Slice(UserArray , func(i, j int) bool {
        return UserArray[i].<givenfield>  < UserArray[j].<givenfield>
    })

CodePudding user response:

I wanted to try sorting a slice of structs by the field's json tag, here is what I ended up having, in case it helps anyone:

package main

import (
    "fmt"
    "reflect"
    "sort"
)

func sortBy(jsonField string, arr []num) {
    if len(arr) < 1 {
        return
    }

    // first we find the field based on the json tag
    valueType := reflect.TypeOf(arr[0])

    var field reflect.StructField

    for i := 0; i < valueType.NumField(); i   {
        field = valueType.Field(i)

        if field.Tag.Get("json") == jsonField {
            break
        }
    }

    // then we sort based on the type of the field
    sort.Slice(arr, func(i, j int) bool {
        v1 := reflect.ValueOf(arr[i]).FieldByName(field.Name)
        v2 := reflect.ValueOf(arr[j]).FieldByName(field.Name)

        switch field.Type.Name() {
        case "int":
            return int(v1.Int()) < int(v2.Int())
        case "string":
            return v1.String() < v2.String()
        case "bool":
            return !v1.Bool() // return small numbers first
        default:
            return false // return unmodified
        }
    })

    fmt.Printf("\nsort by %s:\n", jsonField)
    prettyPrint(arr)
}

func prettyPrint(arr []num) {
    for _, v := range arr {
        fmt.Printf("% v\n", v)
    }
}

type num struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
    Big  bool   `json:"big"`
}

func main() {

    userArray := []num{
        {1, "one", false},
        {5, "five", false},
        {40, "fourty", true},
        {9, "nine", false},
        {60, "sixty", true},
    }

    fmt.Println("original:")
    prettyPrint(userArray)

    sortBy("id", userArray[:])
    sortBy("name", userArray[:])
    sortBy("big", userArray[:])

}
original:
{Id:1   Name:one     Big:false}
{Id:5   Name:five    Big:false}
{Id:40  Name:fourty  Big:true}
{Id:9   Name:nine    Big:false}
{Id:60  Name:sixty   Big:true}

sort by id
{Id:1   Name:one     Big:false}
{Id:5   Name:five    Big:false}
{Id:9   Name:nine    Big:false}
{Id:40  Name:fourty  Big:true}
{Id:60  Name:sixty   Big:true}

sort by name
{Id:5   Name:five    Big:false}
{Id:40  Name:fourty  Big:true}
{Id:9   Name:nine    Big:false}
{Id:1   Name:one     Big:false}
{Id:60  Name:sixty   Big:true}

sort by big
{Id:1   Name:one     Big:false}
{Id:9   Name:nine    Big:false}
{Id:5   Name:five    Big:false}
{Id:40  Name:fourty  Big:true}
{Id:60  Name:sixty   Big:true}

CodePudding user response:

There are two parts to the problem: finding the field given the JSON name and sorting by the field.

Let's start with the code for the second part, sort a slice by field name. Here's an function that sorts a slice of struct or slice of pointer to struct for any struct type. See the commentary for details.

// sortByField sorts slice by the named field.
// The slice argument must be a slice of struct or
// a slice of pointer to struct.
func sortByField(slice interface{}, fieldName string) error {
    v := reflect.ValueOf(slice)
    if v.Kind() != reflect.Slice {
        return fmt.Errorf("got %T, expected slice", slice)
    }

    // Get slice element type.

    t := v.Type().Elem()

    // Handle pointer to struct.

    indirect := t.Kind() == reflect.Ptr
    if indirect {
        t = t.Elem()
    }

    if t.Kind() != reflect.Struct {
        return fmt.Errorf("got %T, expected slice of struct or pointer to struct", slice)
    }

    // Find the field.

    sf, ok := t.FieldByName(fieldName)
    if !ok {
        return fmt.Errorf("field name %s not found", fieldName)
    }

    // Create a less function based on the field's kind.

    var less func(a, b reflect.Value) bool
    switch sf.Type.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        less = func(a, b reflect.Value) bool { return a.Int() < b.Int() }
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
        less = func(a, b reflect.Value) bool { return a.Uint() < b.Uint() }
    case reflect.Float32, reflect.Float64:
        less = func(a, b reflect.Value) bool { return a.Float() < b.Float() }
    case reflect.String:
        less = func(a, b reflect.Value) bool { return a.String() < b.String() }
    case reflect.Bool:
        less = func(a, b reflect.Value) bool { return !a.Bool() && b.Bool() }
    default:
        return fmt.Errorf("field type %s not supported", sf.Type)
    }

    // Sort it!

    sort.Slice(slice, func(i, j int) bool {
        a := v.Index(i)
        b := v.Index(j)
        if indirect {
            a = a.Elem()
            b = b.Elem()
        }
        a = a.FieldByIndex(sf.Index)
        b = b.FieldByIndex(sf.Index)
        return less(a, b)
    })
    return nil
}

Mapping the JSON name to a field is complicated. The program needs to handle the following in the general case: fields promoted by embedding and any conflicts that arise, case insensitivity, elided JSON name, etc. Here's a function that handles the simple case in the question:

func simpleJSONToFieldName(t reflect.Type, name string) (string, bool) {
    for i := 0; i < t.NumField(); i   {
        sf := t.Field(i)
        n := strings.Split(sf.Tag.Get("json"), ",")[0]
        if n == name {
            return sf.Name, true
        }
    }
    return "", false
}

Here's how to put it all together:

 var UserArray []user

 jsonName := request.FormValue("sort")
 fieldName, ok := simpleJSONToFieldName(reflect.TypeOf(user{}), jsonName)
 if !ok {
     // TODO: handle bad input
 }
 if err := sortByField(UserArray, fieldName); err != nil {
     // TODO: handle error
 }

Run an example on the playground.

  • Related