3
\$\begingroup\$

For an API I have defined a "Client" struct which contains all the fields the client has in the database. When a GET request is made with the client ID the whole struct(The struct contains about 60 fields of different types) is returned with all available data. The request can be customized with a querystring "fields" which allow the user to return specific "Client" fields only regardless if they are empty, so omitempty or "-" will not work for my usecase.

GO playground example: https://play.golang.org/p/NTGxGQFz7-Y

type Client struct {
   Firstname   string `json:"firstname"`
   AltAddress  string `json:"alt_address"`
   Lastname    string `json:"lastname"`
}

Example: GET GET /clients/id/1

Response:

{ 
  "firstname":   "Michael",
  "alt_address": "",
  "lastname":    "Smith"
}

Example: GET with querystring Get /clients/id/1?fields=firstname, alt_address

Response:

{ 
  "firstname":   "Michael",
  "alt_address": ""
}

I have this method(objectToFilteredMap) which converts an interface(struct for example) to a map, removes the fields that are NOT defined in "fieldNames" and outputs it as a json object. But I feel like I am using to many "for" loops. Is there a more efficient way to filter out the unneeded fields? Or any other improvements would be appreciated.

var (
    regex = regexp.MustCompile(`^\w+$`)
)

func inSlice(slice []string, val string) bool {
    for _, item := range slice {
        if item == val {
            return true
        }
    }
    return false
}

func toMap(obj interface{}, fieldNames []string) (map[string]interface{}, error) {
    for k, f := range fieldNames {
        f = strings.TrimSpace(f)
        if !regex.Match([]byte(f)) {
            return nil, fmt.Errorf("Invalid field name: `%s`", f)
        }
        fieldNames[k] = strings.ToLower(f)
    }

    j, err := json.Marshal(obj)
    if err != nil {
        return nil, err
    }

    objMap := map[string]interface{}{}
    err = json.Unmarshal(j, &objMap)
    if err != nil {
        return nil, err
    }

    for k := range objMap {
        if !inSlice(fieldNames, k) {
            delete(objMap, k)
        }
    }

    if len(objMap) == 0 {
        return nil, fmt.Errorf("field(s) not found")
    }

    return objMap, nil
}
\$\endgroup\$
6
  • 1
    \$\begingroup\$ Your question title should explain what your code does, not what you are attempting to improve in your code. \$\endgroup\$ Sep 24, 2020 at 14:42
  • \$\begingroup\$ @FreezePhoenix I can't think of a good title. XD Get client information with GET request in GO API? Would that be better? \$\endgroup\$
    – Izzy
    Sep 24, 2020 at 14:46
  • \$\begingroup\$ Is that what your code does? \$\endgroup\$ Sep 24, 2020 at 14:46
  • \$\begingroup\$ I get where this is going, title is changed. \$\endgroup\$
    – Izzy
    Sep 24, 2020 at 14:47
  • \$\begingroup\$ Can you change the db query? If so you could check that the requested api fields are columns (or more likely valid keys in a map between api field and table columns) and the dump a query with just those fields into your 60 item struct with json:"[name],omitempty" tagged so it only contains the fields you want to return when you marshall it. Not a direct review of your code but another possible approach. Anything that's starting to get dynamic with lots of map[string]interface{} is going to feel a bit off in Go compared to Python or other dynamically typed languages \$\endgroup\$
    – Coupcoup
    Sep 24, 2020 at 16:56

1 Answer 1

4
\$\begingroup\$

If you have m fields and n names then you are iterating O(m * n) times. For all fields and names, circa 60 * 60 = 3600 times. Only loop O(m) times:

for field, _ := range m {
    if !names[field] {
        delete(m, field)
    }
}

With some improvements:

package main

import (
    "encoding/json"
    "fmt"
    "regexp"
    "strings"
)

var fieldNameRegex = regexp.MustCompile(`^\w+$`)

func structToMap(client interface{}, fieldNames []string) (map[string]interface{}, error) {
    names := make(map[string]bool, len(fieldNames))
    for _, name := range fieldNames {
        name = strings.TrimSpace(name)
        if !fieldNameRegex.MatchString(name) {
            return nil, fmt.Errorf("Invalid field name: `%s`", name)
        }
        name = strings.ToLower(name)
        names[name] = true
    }

    j, err := json.Marshal(client)
    if err != nil {
        return nil, err
    }
    m := map[string]interface{}{}
    err = json.Unmarshal(j, &m)
    if err != nil {
        return nil, err
    }
    for field, _ := range m {
        if !names[field] {
            delete(m, field)
        }
    }
    if len(m) != len(names) {
        return nil, fmt.Errorf("field(s) not found")
    }
    return m, nil
}

type Client struct {
    AltAddress string `json:"alt_address"`
    Firstname  string `json:"firstname"`
    Lastname   string `json:"lastname"`
}

func main() {
    client := Client{
        AltAddress: "HalLo",
        Lastname:   "smith",
    }

    m, err := structToMap(client, []string{"alt_address", "firstname"})
    if err != nil {
        fmt.Println("error1:", err)
        return
    }
    j, err := json.Marshal(m)
    if err != nil {
        fmt.Println("error2:", err)
        return
    }
    fmt.Println(string(j))
}

Playground: https://play.golang.org/p/Qf8bdygEcVT

{"alt_address":"HalLo","firstname":""}
\$\endgroup\$
2
  • \$\begingroup\$ Thank you! This is these are the improvements I was looking for. \$\endgroup\$
    – Izzy
    Sep 25, 2020 at 5:59
  • \$\begingroup\$ It is worth improving a section of your answer right after With some improvements: paragraph and add some explanation which actual improvements you've made \$\endgroup\$
    – sineemore
    Sep 25, 2020 at 22:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.