gostructcompareslicecmp

How to semantically compare structs with nested slices regardless of elements' order in Go


Given the next type structs definition:

type A struct {
    Id  int
    Bs  []B
    Sub C
}

type B struct {
    Id  int
    Str string
}

type C struct {
    Id int
    Ds []D
}

type D struct {
    Id  int
    Num int
}

I would like to test if next two instances of A are semantically equal, regardless of the order of the slice elements in all hierarchy levels.

var want = &A{
    Id: 1,
    Bs: []B{{Id: 10, Str: "b10"}, {Id: 20, Str: "b20"}},
    Sub: C{
        Id: 100,
        Ds: []D{{Id: 101, Num: 1001}, {Id: 102, Num: 1002}},
    },
}

var got = &A{
    Id: 1,
    Bs: []B{{Id: 20, Str: "b20"}, {Id: 10, Str: "b10"}},
    Sub: C{
        Id: 100,
        Ds: []D{{Id: 102, Num: 1002}, {Id: 101, Num: 1001}},
    },
}

The assert comparison should return true


Solution

  • The package cmp is intended to be a more powerful and safer alternative to reflect.DeepEqual for comparing whether two values are semantically equal.

    Here is a full implementation of semantic equal structure comparison regardless of slice elements' order at all hierarchy levels.

    file source.go

    package main
    
    type A struct {
        Id  int
        Bs  []B
        Sub C
    }
    
    type B struct {
        Id  int
        Str string
    }
    
    type C struct {
        Id int
        Ds []D
    }
    
    type D struct {
        Id  int
        Num int
    }
    
    func NewA() *A {
        return &A{
            Id: 1,
            Bs: []B{{Id: 20, Str: "b20"}, {Id: 10, Str: "b10"}},
            Sub: C{
                Id: 100,
                Ds: []D{{Id: 102, Num: 1002}, {Id: 101, Num: 1001}},
            },
        }
    }
    

    file source_test.go

    package main
    
    import (
        "fmt"
        "testing"
    
        "github.com/google/go-cmp/cmp"
        "github.com/google/go-cmp/cmp/cmpopts"
    )
    
    var want = &A{
        Id: 1,
        Bs: []B{{Id: 10, Str: "b10"}, {Id: 20, Str: "b20"}},
        Sub: C{
            Id: 100,
            Ds: []D{{Id: 101, Num: 1001}, {Id: 102, Num: 1002}},
        },
    }
    
    func TestNewA(t *testing.T) {
        got := NewA()
        less := func(x, y any) bool {
            switch xv := x.(type) {
            case B:
                yv := y.(B)
                return fmt.Sprintf("%d-%s", xv.Id, xv.Str) < fmt.Sprintf("%d-%s", yv.Id, yv.Str)
            case D:
                yv := y.(D)
                return fmt.Sprintf("%d-%d", xv.Id, xv.Num) < fmt.Sprintf("%d-%d", yv.Id, yv.Num)
            default:
                return false
            }
        }
        if diff := cmp.Diff(want, got, cmpopts.SortSlices(less)); diff != "" {
            t.Errorf("mismatch:\n%s", diff)
        }
    }