25

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?

5 Answers 5

20

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

Sign up to request clarification or add additional context in comments.

1 Comment

this answer gives specific solution to the problem
14

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

2 Comments

How to compare two interfaces except some fields? Because I can't call of reflect.Value.FieldByName on interface Value. The original requirement is that I need to compare two json string except some fields, so I unmarshal them to interfaces and compare the interfaces.
@icza maybe this answer is different with cmp package nowadays ?
8

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

Comments

4

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

Comments

0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.