Gaurav Raghuvanshy
Gaurav Raghuvanshy

Reputation: 431

Compare structs except one field golang

I am comparing two structs and want to ignore a single field while doing so.

type test struct {
  name string
  time string
} 

func main() {
  a := test{"testName", time.Now().Format(time.UnixTime)}
  // after some time
  b := test{"testName", time.Now().Format(time.UnixTime)}

  fmt.Println(a.Name == b.Name) \\ returns true Desired outcome
  fmt.Println(reflect.DeepEqual(a,b)) \\ returns false

}

reflect.DeepEqual() does not allow for us to ignore a field and have manually done the comparison one field at a time.

What is the idiomatic way to go about this?

Upvotes: 25

Views: 34326

Answers (5)

csknk
csknk

Reputation: 2039

For testing purposes, you can use cmpopts - this allows (exported) fields to be ignored when cmp.Equal from the cmp package is used.

func main() {
    a := test{"testName", time.Now().Local().String()}
    // after some time
    b := test{"testName", time.Now().Local().String()}

    fmt.Printf("a: %#v\nb: %#v\n", a, b)

    if !cmp.Equal(a, b, cmpopts.IgnoreFields(test{}, "Time")) {
        log.Fatalf("mismatching data")
    }
}

Upvotes: 8

Smruti Parikh
Smruti Parikh

Reputation: 1

Using reflect, we can get the kind and perform a deep check on every kind.

    func EqualExcept(expected, actual interface{}, exceptFields ...string) bool {
    valE := reflect.ValueOf(expected)
    valA := reflect.ValueOf(actual)

    switch valE.Kind() {
    case reflect.Array, reflect.Slice:
        {
            for i := 0; i < valE.Len(); i++ {
                e := valE.Index(i).Interface()
                a := valA.Index(i).Interface()
                if !EqualExcept(e, a, exceptFields...) {
                    return false
                }
            }
            return true
        }
    case reflect.Map:
        {
            for i := 0; i < len(valE.MapKeys()); i++ {
                key := valE.MapKeys()[i]
                keyStr := toString(key)
                if stringInSlice(keyStr, exceptFields) {
                    continue
                }

                e := valE.MapIndex(key)
                a := valA.MapIndex(key)

                if !EqualExcept(e.Interface(), a.Interface(), exceptFields...) {
                    return false
                }
            }
            return true
        }
    case reflect.Struct, reflect.Interface:
        {
            for i := 0; i < valE.NumField(); i++ {
                typeField := valE.Type().Field(i)
                if typeField.Tag == "json:\"-\"" || stringInSlice(typeField.Name, exceptFields) {
                    continue
                }

                e := valE.Field(i)
                a := valA.FieldByName(typeField.Name)

                if !EqualExcept(e.Interface(), a.Interface(), exceptFields...) {
                    return false
                }
            }
            return true
        }

    case reflect.Ptr:
        {
            if valE.IsNil() {
                return valA.IsNil()
            }

            e := valE.Elem()
            a := reflect.Indirect(valA)
            return EqualExcept(e.Interface(), a.Interface(), exceptFields...)
        }

    case reflect.String, reflect.Int, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Bool:
        return expected == actual

    default:
        return valE.Interface() == valA.Interface()
    }
}

func toString(v reflect.Value) string {
    bytes, _ := json.Marshal(v.Interface())
    return string(bytes)
}

func stringInSlice(a string, list []string) bool {
    for _, b := range list {
        if b == a {
            return true
        }
    }
    return false
}

Here is the demo:

Go Playground

Upvotes: 0

icza
icza

Reputation: 417592

1. With embedding

One option would be to group fields that should take part in the comparison into a different struct which you can embed in your original. When doing the comparison, just compare the embedded fields:

type Person struct {
    Name string
    Age  int
}

type Doc struct {
    Person
    Created time.Time
}

func main() {
    d1 := Doc{
        Person:  Person{"Bob", 21},
        Created: time.Now(),
    }
    time.Sleep(time.Millisecond)
    d2 := Doc{
        Person:  Person{"Bob", 21},
        Created: time.Now(),
    }

    fmt.Println(d1 == d2)               // false
    fmt.Println(d1.Person == d2.Person) // true
}

Try it on the Go Playground.

If the struct does not contain pointers, slices, maps etc., you can simply compare the embedded values using ==. Otherwise use reflect.DeepEqual() to compare them.

2. Temporarily modifying the excludable fields

You may also choose to temporarily modify the fields you don't want to compare: make them equal so the comparison result will only depend on the rest of the fields:

a := test{"testName", time.Now().Format(time.StampMilli)}
time.Sleep(time.Millisecond)
b := test{"testName", time.Now().Format(time.StampMilli)}

// Save and make excluded fields equal:
old := a.time
a.time = b.time

fmt.Println(a.name == b.name)        // true
fmt.Println(reflect.DeepEqual(a, b)) // true

// Restore:
a.time = old

Try it on the Go Playground.

Another variation is to make a copy of one of the struct values, and modify and compare that to the other, so no need to restore the original, also this is "more concurrent-friendly":

// Make copies and make excluded fields equal:
a2 := a
a2.time = b.time

fmt.Println(a2.name == b.name)        // true
fmt.Println(reflect.DeepEqual(a2, b)) // true

Try this on the Go Playground.

3. Implement your own, custom comparison

If you can't or don't want to go with the above solutions, you can always create your own:

func compare(a, b test) bool {
    return a.name == b.name
}


fmt.Println(a.name == b.name) // true
fmt.Println(compare(a, b))    // true

Try this on the Go Playground.

Notes:

This isn't "friendly" at first, as the custom compare() function requires you to check all involved fields, but its implementation may use the above methods, e.g. (try it on the Go Playground):

func compare(a, b test) bool {
    a.time = b.time // We're modifying a copy, so no need to make another copy
    return reflect.DeepEqual(a, b)
}

You could also pass pointers to compare() to avoid copying the original struct, e.g. (try it on the Go Playground):

fmt.Println(a.name == b.name) // true
fmt.Println(compare(&a, &b))  // true

func compare(a, b *test) bool {
    a2 := new(test)
    *a2 = *a
    a2.time = b.time
    return reflect.DeepEqual(a2, b)
}

Upvotes: 14

Eugene Lisitsky
Eugene Lisitsky

Reputation: 12845

If your fields are exported, you may use reflect to check them. E.g.:

package main

import (
    "fmt"
    "reflect"
)

type Foo struct {
    Name string
    Date int
}

func (f *Foo) EqualExcept(other *Foo, ExceptField string) bool {
    val := reflect.ValueOf(f).Elem()
    otherFields := reflect.Indirect(reflect.ValueOf(other))

    for i := 0; i < val.NumField(); i++ {
        typeField := val.Type().Field(i)
        if typeField.Name == ExceptField {
            continue
        }

        value := val.Field(i)
        otherValue := otherFields.FieldByName(typeField.Name)

        if value.Interface() != otherValue.Interface() {
            return false
        }
    }
    return true
}

func main() {
    f := &Foo{
        "Drew",
        30,
    }

    f2 := &Foo{
        "Drew",
        50,
    }

    fmt.Println(f.EqualExcept(f2, "Date"))
    fmt.Println(f.EqualExcept(f2, "Name"))

}

https://play.golang.org/p/DlhE8aZGwa

Upvotes: 4

Alexander Trakhimenok
Alexander Trakhimenok

Reputation: 6278

Idiomatic way would be to implement your own func (o MyStruct) Equal(o2 MyStruct) bool method.

Upvotes: 20

Related Questions