在golang中我們可以輕松地通過==
來判斷兩個數組(array)是否相等,但遺憾的是slice並沒有相關的運算符,當需要判斷兩個slice是否相等時我們只能另尋捷徑了。
slice相等的定義
我們選擇最常見的需求,也就是當兩個slice的類型和長度相同,且相等下標的值也是相等的,比如:
a := []int{1, 2, 3}
b := []int{1, 2, 3}
c := []int{1, 2}
d := []int{1, 3, 2}
上述代碼中a
和b
是相等的,c
因為長度和a
不同所以不相等,d
因為元素的排列順序和a
不同所以也不相等。
判斷兩個[]byte是否相等
為什么要單獨將[]byte列舉出來呢?
因為標准庫提供了優化的比較方案,不再需要我們造輪子了:
package main
import (
"bytes"
"fmt"
)
func main() {
a := []byte{0, 1, 3, 2}
b := []byte{0, 1, 3, 2}
c := []byte{1, 1, 3, 2}
fmt.Println(bytes.Equal(a, b))
fmt.Println(bytes.Equal(a, c))
}
運行結果如下:
使用reflect判斷slice是否相等
在判斷類型不是[]byte的slice時,我們還可以借助reflect.DeepEqual
,它用於深度比較兩個對象包括它們內部包含的元素是否都相等:
func DeepEqual(x, y interface{}) bool
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.
...
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.
這段話的意思不難理解,和我們在本文最開始時討論的如何確定slice相等的原則是一樣的,只不過它借助了一點運行時的“黑魔法”。
看例子:
package main
import (
"fmt"
"reflect"
)
func main() {
a := []int{1, 2, 3, 4}
b := []int{1, 3, 2, 4}
c := []int{1, 2, 3, 4}
fmt.Println(reflect.DeepEqual(a, b))
fmt.Println(reflect.DeepEqual(a, c))
}
手寫判斷
在golang中使用reflect通常需要付出性能代價,如果我們確定了slice的類型,那么自己實現slice的相等判斷相對來說也不是那么麻煩:
func testEq(a, b []int) bool {
// If one is nil, the other must also be nil.
if (a == nil) != (b == nil) {
return false;
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
測試代碼:
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4}
b := []int{1, 3, 2, 4}
c := []int{1, 2, 3, 4}
fmt.Println(testEq(a, b))
fmt.Println(testEq(a, c))
}
運行結果:
下面我們對后兩種方案做個簡單的性能測試,我們測試兩個不相等但很相似的擁有20個元素的slice,這是在日常開發中較常見的情景:
func BenchmarkTestEq(b *testing.B) {
a := []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
c := []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = testEq(a, c)
}
}
func BenchmarkDeepEqual(b *testing.B) {
a := []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
c := []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = reflect.DeepEqual(a, c)
}
}
當然這個測試只能反應出有限的信息,正常情況下應該給出更全面的測試用例。不過在我們的演示中反射仍然付出了驚人的性能代價:
如果我們把slice的長度設為1000,那么差距就會更加明顯:
func genDiffSlice(size int) ([]uint32, []uint32) {
a := make([]uint32, 0, size)
rand.Seed(time.Now().UnixNano())
for i := 0; i < size; i++ {
a = append(a, rand.Uint32())
}
b := make([]uint32, len(a))
copy(b, a)
b[len(b)-1] = rand.Uint32()
return a, b
}
func BenchmarkTestEq2(b *testing.B) {
a, c := genDiffSlice(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = testEq(a, c)
}
}
func BenchmarkDeepEqual2(b *testing.B) {
a, c := genDiffSlice(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = reflect.DeepEqual(a, c)
}
}
自己手寫判斷的性能更好,但是有個顯而易見的弊端,當我們有多種類型的slice時我們就不得不編寫不同版本的testEq
,而它們唯一的不同僅僅只有slice的類型。
不過等到go2的泛型可以使用的時候,這樣的弊端也就不復存在了,現在我們需要的是在代碼的復雜度和運行性能上做出權衡。