当前位置: 首页 > 工具软件 > Go Circuit > 使用案例 >

Go结构体能比较吗?Go的类型比较,==或DeepEqual比较!

吕霍英
2023-12-01


前言

类型比较是编程中离不开的手段,在Go里面的比较涉及到普通的数值类型,切片数组,map,结构体,函数,指针,接口等,有些可以单独比较,有些不能,有些可以组合起来比较,有些组合起来不能比较诸如此类,由于个人水平有限,下面就这些问题做些简单探讨,如有不当之处,望不吝指教!


一、Go语言的比较符语法介绍

Golang的比较符有六种,它们比较的都是值类型;

运算符说明范例结果
==相等4 == 4true
!=不等于4 != 4false
>大于4 > 4false
>=大于等于4 >= 4true
<小于4 < 4false
<=小于等于4 <= 4true

由于比较符比较的都是值类型,所以在比较引用类型的时候就会出现无法比较的情况。

1.不能使用比较符比较

  1. 切片/数组
  2. Map
  3. 函数
  4. 拥有不同成员或者拥有不能比较的成员的结构体
  5. 拥有不同函数的接口
  6. 函数
  7. 不同类型的结构体指针(同类型的结构体对象:new出来的,可以比较,但是由于比较的是地址,所以不准确)

2.可以使用比较符比较的特殊类型

  1. 相同类型的结构体,且结构体中的成员类型个数完全相同,且不能含有无法比较的成员。
    案例:
//可以比较的结构体,因为成员类型个数完全一致,且不存在无法比较的类型
type AA struct {
	a int
}
type AB struct {
	a int
}
func main() {
	aa := AA{a: 5}
	ab := AB{a: 5}
	//可以比较
	if aa == AA(ab) {//这里因为结构体不是同一个,所以强转成同一个
		fmt.Println(true)
	} else {
		fmt.Println(false)
	}
}
//不可以比较的结构体,因为成员个数不一样
type AA struct {
	a int
}
type AB struct {
	a int
	b int
}
func main() {
	aa := AA{a: 5}
	ab := AB{a: 5}
	//报错,不能比较
	if aa == AA(ab) {
		fmt.Println(true)
	} else {
		fmt.Println(false)
	}
}
//用下面两个AA类型结构体不可以比较,因为存在无法比较的类型
type AA struct {
	a int
	b []int //切片不能使用比较符比较
	m map[string]int //字典不能使用比较符比较
}
func main() {
	aa := AA{}
	//报错,不能比较
	if aa == aa {
		fmt.Println(true)
	} else {
		fmt.Println(false)
	}
}


  1. 拥有包含关系的两个接口
    案例:
type T1 interface {
	a() []int
}
type T2 interface {
	a() []int
	b() []int
}
	var t1 T1
	var t2 T2
	if t1 == t2 { //由于T2对T1是包集(T1中的函数全部为T2的子集,或者T1中没有函数),所以能够比较,结果为true;
		fmt.Println(true)
	} else {
		fmt.Println(false)
	}

二、DeepEqual比较

上面的代码体现了运算符比较的局限性,那么如果我们想要比较拥有切片等不能比较成员的结构体怎么办呢?(注意:结构体的比较必须为同一个结构体类型,不同类型的需要强制转换)答案是使用反射包的DeepEqual方法!

1.DeepEqual源码

代码如下:

// DeepEqual reports whether x and y are ``deeply equal,'' defined as follows.
// Two values of identical type are deeply equal if one of the following cases applies.
// Values of distinct types are never deeply equal.
//
// Array values are deeply equal when their corresponding elements are deeply equal.
//
// Struct values are deeply equal if their corresponding fields,
// both exported and unexported, are deeply equal.
//
// Func values are deeply equal if both are nil; otherwise they are not deeply equal.
//
// Interface values are deeply equal if they hold deeply equal concrete values.
//
// Map values are deeply equal when all of the following are true:
// they are both nil or both non-nil, they have the same length,
// and either they are the same map object or their corresponding keys
// (matched using Go equality) map to deeply equal values.
//
// Pointer values are deeply equal if they are equal using Go's == operator
// or if they point to deeply equal values.
//
// Slice values are deeply equal when all of the following are true:
// they are both nil or both non-nil, they have the same length,
// and either they point to the same initial entry of the same underlying array
// (that is, &x[0] == &y[0]) or their corresponding elements (up to length) are deeply equal.
// Note that a non-nil empty slice and a nil slice (for example, []byte{} and []byte(nil))
// are not deeply equal.
//
// Other values - numbers, bools, strings, and channels - are deeply equal
// if they are equal using Go's == operator.
//
// In general DeepEqual is a recursive relaxation of Go's == operator.
// However, this idea is impossible to implement without some inconsistency.
// Specifically, it is possible for a value to be unequal to itself,
// either because it is of func type (uncomparable in general)
// or because it is a floating-point NaN value (not equal to itself in floating-point comparison),
// or because it is an array, struct, or interface containing
// such a value.
// On the other hand, pointer values are always equal to themselves,
// even if they point at or contain such problematic values,
// because they compare equal using Go's == operator, and that
// is a sufficient condition to be deeply equal, regardless of content.
// DeepEqual has been defined so that the same short-cut applies
// to slices and maps: if x and y are the same slice or the same map,
// they are deeply equal regardless of content.
//
// As DeepEqual traverses the data values it may find a cycle. The
// second and subsequent times that DeepEqual compares two pointer
// values that have been compared before, it treats the values as
// equal rather than examining the values to which they point.
// This ensures that DeepEqual terminates.
func DeepEqual(x, y interface{}) bool {
	if x == nil || y == nil {
		return x == y
	}
	v1 := ValueOf(x)
	v2 := ValueOf(y)
	if v1.Type() != v2.Type() {
		return false
	}
	return deepValueEqual(v1, v2, make(map[visit]bool), 0)
}

第一眼我们通过注释可以知道,在成员变量都是可以使用==比较的情况下,Array,
Struct,Func,Interface,Map都是可以使用DeepEqual方法来比较的。
看完注释之后,我们来看看源码它具体是怎么实现比较功能的!

在DeepEqual里面我们发现它有判空,且有一方为nil的情况下,直接使用==比较,所以在使用该方法的时候我们也不用额外的注意给的对象如果是nil会报空指针错误这样的事了。
并且另一个很直接的判断是:

v1 := ValueOf(x)
	v2 := ValueOf(y)
	if v1.Type() != v2.Type() {
		return false
	}

它直接判断了类型如果不是同一个则返回false,所以在使用该方法的时候如果结构体不是同一个的话,我们需要强制转换成同一个,否则永远是false。
我们接着进入deepValueEqual(v1, v2, make(map[visit]bool), 0)函数:

func deepValueEqual(v1, v2 Value, visited map[visit]bool, depth int) bool {
	if !v1.IsValid() || !v2.IsValid() {
		return v1.IsValid() == v2.IsValid()
	}
	if v1.Type() != v2.Type() {
		return false
	}

	// if depth > 10 { panic("deepValueEqual") }	// for debugging

	// We want to avoid putting more in the visited map than we need to.
	// For any possible reference cycle that might be encountered,
	// hard(v1, v2) needs to return true for at least one of the types in the cycle,
	// and it's safe and valid to get Value's internal pointer.
	hard := func(v1, v2 Value) bool {
		switch v1.Kind() {
		case Map, Slice, Ptr, Interface:
			// Nil pointers cannot be cyclic. Avoid putting them in the visited map.
			return !v1.IsNil() && !v2.IsNil()
		}
		return false
	}

	if hard(v1, v2) {
		// For a Ptr or Map value, we need to check flagIndir,
		// which we do by calling the pointer method.
		// For Slice or Interface, flagIndir is always set,
		// and using v.ptr suffices.
		ptrval := func(v Value) unsafe.Pointer {
			switch v.Kind() {
			case Ptr, Map:
				return v.pointer()
			default:
				return v.ptr
			}
		}
		addr1 := ptrval(v1)
		addr2 := ptrval(v2)
		if uintptr(addr1) > uintptr(addr2) {
			// Canonicalize order to reduce number of entries in visited.
			// Assumes non-moving garbage collector.
			addr1, addr2 = addr2, addr1
		}

		// Short circuit if references are already seen.
		typ := v1.Type()
		v := visit{addr1, addr2, typ}
		if visited[v] {
			return true
		}

		// Remember for later.
		visited[v] = true
	}

	switch v1.Kind() {
	case Array:
		for i := 0; i < v1.Len(); i++ {
			if !deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
				return false
			}
		}
		return true
	case Slice:
		if v1.IsNil() != v2.IsNil() {
			return false
		}
		if v1.Len() != v2.Len() {
			return false
		}
		if v1.Pointer() == v2.Pointer() {
			return true
		}
		for i := 0; i < v1.Len(); i++ {
			if !deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
				return false
			}
		}
		return true
	case Interface:
		if v1.IsNil() || v2.IsNil() {
			return v1.IsNil() == v2.IsNil()
		}
		return deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1)
	case Ptr:
		if v1.Pointer() == v2.Pointer() {
			return true
		}
		return deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1)
	case Struct:
		for i, n := 0, v1.NumField(); i < n; i++ {
			if !deepValueEqual(v1.Field(i), v2.Field(i), visited, depth+1) {
				return false
			}
		}
		return true
	case Map:
		if v1.IsNil() != v2.IsNil() {
			return false
		}
		if v1.Len() != v2.Len() {
			return false
		}
		if v1.Pointer() == v2.Pointer() {
			return true
		}
		for _, k := range v1.MapKeys() {
			val1 := v1.MapIndex(k)
			val2 := v2.MapIndex(k)
			if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(val1, val2, visited, depth+1) {
				return false
			}
		}
		return true
	case Func:
		if v1.IsNil() && v2.IsNil() {
			return true
		}
		// Can't do better than this:
		return false
	default:
		// Normal equality suffices
		return valueInterface(v1, false) == valueInterface(v2, false)
	}
}

可以很明了的看到第二个switch里面,程序每次都是以第一个数据为起点来判断,然后在每一个分支里面再来细致的比较,整体程序以递归的形式进行处理。
我们以下面这两个结构体为案例来介绍下:

type AA struct {
	a int
	b []int
}
type AB struct {
	a int
	b []int
}
func main() {
	a :=  AA{a: 6,b: []int{2}}
	b := AB{a: 6,b: []int{2,3}}
	fmt.Println(reflect.DeepEqual(a, AA(b)))
}

由于比较的是两个结构体,所以我们在第一次的时候会执行改代码:

case Struct:
		for i, n := 0, v1.NumField(); i < n; i++ {
			if !deepValueEqual(v1.Field(i), v2.Field(i), visited, depth+1) {
				return false
			}
		}
		return true

我们可以看到for循环中是依次的从两个结构体中取成员对象用来进行下一次递归活动,这里有一个点,之前我还不知道,原来如果结构体中的成员类型顺序不一致,会导致强转失败,如下面的两个结构体将不能进行强转:

type AA struct {
	a int
	b []int
}
type AB struct {
	b []int
	a int
}

言归正传,因为我们结构体中的第一个成员都是6,所以第一个成员的变量将会执行下面的代码并返回true:

default:
		// Normal equality suffices
		return valueInterface(v1, false) == valueInterface(v2, false)

然后代码继续返回到case Struct,进行下一次的循环,并进入:

case Slice:
		if v1.IsNil() != v2.IsNil() {
			return false
		}
		if v1.Len() != v2.Len() {
			return false
		}
		if v1.Pointer() == v2.Pointer() {
			return true
		}
		for i := 0; i < v1.Len(); i++ {
			if !deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
				return false
			}
		}
		return true

由于两个结构体中的切片长度不一致,所以这里返回false,然后代码跳转到case Struct:,并最终结果为:false。结果正确,其它的类型map等逻辑也大同小异。

 类似资料: