golang 에서는 byte로 이루어진 slice, array를 비교할 수 있는 다양한 메서드들을 지원한다.
또한 별도로 구현해서 사용하는 방법이 있다. 단, "==" 비교 연산자를 이용해서 []byte 배열(array or slice) 값들을 비교할 수는 없다.
아래와 같이 '==' 연산자를 사용한 경우 "잘못된 연산자(invalid operation)"를 사용했다는 compile error를 만나게 된다.
- Comparing byte slices using '==' comparator
data1 := []byte("abcdefg1234567890!@#$%^&*()안녕하세요GHJKLZXCVB")
data2 := []byte("abcdefg1234567890!@#$%^&*()안녕하세요GHJKLZXCVB")
if data1 == data2 {
fmt.Println(true)
}
- compile result
.\main.go:16:5: invalid operation: data1 == data2 (slice can only be compared to nil)
그렇다면 byte slice, byte array의 값들은 어떻게 비교해야할까?
방법을 하나씩 소개해 보겠다.
가장 손쉽게 활용할 수 있는 방법은 아래와 같이 bytes package에서 제공하는 함수들을 사용하는 것이다.
byte package에서 제공하는 slice, array 비교하는 기능을 제공하는 대표적인 함수로 bytes.Compare(), bytes.Equal(), bytes.EqualFold() 이 존재한다.
그리고 범용적으로 사용 가능한 reflect 패키지의 DeepEqual() 함수도 많이 사용된다. 다만 assertion 체크로직이 필요함으로 범용적으로 사용가능하지만 성능이 떨어진다는 단점을 가지고 있다.
bytes에서 제공하는 함수를 이용하지 않고 비교하는 방법은 대표적으로 for문으로 byte slice 또는 array를 순회하면서 직접 비교하는 방법, 그리고 string으로 변환한 수 equal operator (==)를 사용하거나 strings패키지에서 제공하는 비교 함수를 사용하는 방법들이 있다.
1. bytes.Compare(a, b []byte)
가장 먼저 bytes.Compare()는 아래와 같이 같으면 0, 앞의 인자 값이 작으면 -1, 앞의 인자 값이 크면 1을 리턴해주는 함수 이다. 뒤에서 benchmark test 결과를 공유하겠지만 가장 성능이 우수한 비교 방법이다.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b.
사용 방법은 간단하다. 만약 두 값이 같은지 확인하는 코드라면 아래와 같이 함수의 결과 값이 0인지만 확인하면 된다.
func BytesCompare(data1 []byte, data2 []byte) bool {
if bytes.Compare(data1, data2) == 0 {
return true
}
return false
}
2. bytes.Equal(a, b []byte)
두 번째로는 Equal() 함수를 사용하는 것이다.
func BytesEqual(data1 []byte, data2 []byte) bool {
return bytes.Equal(data1, data2)
}
하지만 이 방법을 사용하는 것은 꽤 멍청한 짓이다. bytes.Equal 함수의 구현을 확인해보면 아래와 같다.
func Equal(a, b []byte) bool {
return string(a) == string(b)
}
충분히 구현 가능한, 그리고 이미 우리가 사용하고 있을지도 모르는 방법을 warpping 해놓은 것과 같다. 바로 string()으로 변환 한 다음 equal operator (==) 를 이용하여 비교하는 방식으로 구현되어있는 함수이다.
이 함수를 사용하는 것이 멍청한 짓이라고 표현한 이유는...해당 함수를 이용하면 함수에 인자를 전달하기 위한 copy 과정이 추가되어 직접 string()으로 변환하여 비교하는 것 보다 성능이 떨어질 수 밖에 없기 때문이다.
이 방법을 사용하려면 차라리.. 아래와 같이 string으로 변환한 후 equal operator(==)를 사용하길 바란다.
3. bytes.EqualFold(a, b []byte)
이번에 소개하는 방법은 성능은 대소문자 등을 구분하지 않는 경우 많이 사용한다. strings패키지에서 string slice, array를 비교할 때 매우 우수한 성능으로 많이 사용되지만 bytes 패키지에서 제공하는 byte slice, array 비교 중에서는 성능이 우수한 편은 아니다.
해당 함수 역시 리턴되는 결과가 bool 타입이므로 별도로 값을 이용해서 비교 를 구현할 필요는 없다.
func BytesEqualFold(data1 []byte, data2 []byte) bool {
return bytes.EqualFold(data1, data2)
}
case#1 - 대소문자가 서로 다른 경우
> 예제
var data1 []byte = []byte("abcdefg1234567890!@#$%^&*()안녕하세요GHJKLZXCVB")
var data2 []byte = []byte("Abcdefg1234567890!@#$%^&*()안녕하세요GHJKLZXCVB")
fmt.Println(BytesEqualFold(data1, data2))
>결과
true
case#2 - 대소문자가 서로 같은 경우 예제
> 예제
var data1 []byte = []byte("abcdefg1234567890!@#$%^&*()안녕하세요GHJKLZXCVB")
var data2 []byte = []byte("abcdefg1234567890!@#$%^&*()안녕하세요GHJKLZXCVB")
fmt.Println(BytesEqualFold(data1, data2))
> 결과
true
case#3 - 완전 다른 문자열끼리의 비교
> 예제
var data1 []byte = []byte("abcdefg1234567890!@#$%^&*()안녕하세요GHJKLZXCVB")
var data2 []byte = []byte("abcdefg1234567890!@#$%^&*()")
fmt.Println(BytesEqualFold(data1, data2))
> 결과
false
4. string 으로 변환 후 operator 으로 사용
이 방법은 위에서 소개했듯이 bytes 패키지에서 제공하는 Equal() 함수에서 사용하는 로직과 동일하다
func CompareToString(data1 []byte, data2 []byte) bool {
return string(data1) == string(data2)
}
string으로 변환하는 과정 때문에 성능에 많은 문제가 있을 것 같지만, 실제로는 그렇지 않다. bytes.Compare() 를 사용하는 것과 크게 차이가 없으며, 이번 포스팅에서 소개하는 방법 중에 2번째로 빠른 방법이며, bytes에서 제공하는 Equal() 함수를 사용하는 것보다 미세하게 성능이 더 빠르다.
만약 bytes에서 제공하고 있는 Equal() 함수를 사용하고 있다면 차라리 위와 같이 직접 구현해서 사용하는 것이 더 빠른 성능을 보장한다.
5. for 문을 이용하여 값 비교
다섯번째로는 for 문을 통해 두개의 byte array 또는 slice를 순회하면서 값을 비교하다가 서로 다른 값을 만나는 경우 false를 return 하고, 다른 값을 만나지 않는 경우 true를 return 하는 방식이다.
func RangeLoop(data1 []byte, data2 []byte) bool {
if len(data1) != len(data2) {
return false
}
for n, data := range data1 {
if data != data2[n] {
return false
}
}
return true
}
만약 첫번째 문자부터 다르다면 가장 빠른 방법이 되겠지만 같은 문자열로 판명되는 경우 매우 느린 성능을 보이게 되는 방법이다. 대부분의 처음 시작하는 고퍼들이 실수하는 부분이 string으로 변환한 다음 equal operator로 비교하는 것보다 빠를 것이라고 생각하여 구현해서 사용하는 대표적인 방법으로 대표적인 worst case 중에 하나이다.
6. reflect.DeepEqual() 함수를 이용하여 비교
byte 뿐만 아니라 모든 타입의 slice, array 를 비교할 수 있는 매우 편의성이 높은 방법 중 하나이다.
단, 성능이 매우 느린 것은 최대 단점 중 하나이다.
편의성과 성능을 서로 교환했다고할까... 어쩜 이렇게 성능이 안나오는지... 사용했다가 나중에 배신감을 뚝뚝 느끼게 되는 함수이다.
해당 함수 역시 리턴타입이 bool 이기 때문에 별도로 결과에 대한 후 비교 처리를 필요로 하지 않는다.
func ReflectDeepEqual(data1 []byte, data2 []byte) bool {
return reflect.DeepEqual(data1, data2)
}
Benchmark Test
위에서 소개하면서 구현한 함수들을 이용하여 Benchmark test 를 수행하여 성능을 측정해보았다.
모두 함수화 하였고, 함수에 전달하기까지 코드에서 발생하는 변수의 copy의 수는 동일하게 발생하도록 조건으로 셋팅하였다.
> benchmark test code
var data1 []byte = []byte("abcdefg1234567890!@#$%^&*()안녕하세요ZXCVBNMZ")
var data2 []byte = []byte("abcdefg1234567890!@#$%^&*()안녕하세요ZXCVBNMZ")
func BenchmarkBytesCompare(b *testing.B) {
for i := 0; i < b.N; i++ {
BytesCompare(data1, data2)
}
}
func BenchmarkBytesEqual(b *testing.B) {
for i := 0; i < b.N; i++ {
BytesEqual(data1, data2)
}
}
func BenchmarkBytesEqualFold(b *testing.B) {
for i := 0; i < b.N; i++ {
BytesEqualFold(data1, data2)
}
}
func BenchmarkCompareToString(b *testing.B) {
for i := 0; i < b.N; i++ {
CompareToString(data1, data2)
}
}
func BenchmarkRangeLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
RangeLoop(data1, data2)
}
}
func BenchmarkReflectDeepEqual(b *testing.B) {
for i := 0; i < b.N; i++ {
ReflectDeepEqual(data1, data2)
}
}
> Benchmark Test Result
# go test -v -bench .
goos: windows
goarch: amd64
pkg: tistoryexample/bytecomparator
cpu: AMD Ryzen 9 5900X 12-Core Processor
BenchmarkBytesCompare
BenchmarkBytesCompare-24 279758036 4.321 ns/op
BenchmarkBytesEqual
BenchmarkBytesEqual-24 218490802 5.334 ns/op
BenchmarkBytesEqualFold
BenchmarkBytesEqualFold-24 18571826 63.39 ns/op
BenchmarkCompareToString
BenchmarkCompareToString-24 229016505 5.271 ns/op
BenchmarkRangeLoop
BenchmarkRangeLoop-24 48852972 23.90 ns/op
BenchmarkReflectDeepEqual
BenchmarkReflectDeepEqual-24 10363428 115.9 ns/op
PASS
ok tistoryexample/bytecomparator 8.908s
Benchmark 테스트 결과 bytes패키지에서 제공하는 Compare() 함수가 가장 빠른 성능을 보이는 것으로 확인하였다.
그 다음은 string 으로 변환한 후 Equal Operator (==) 를 이용하여 비교하도록 직접 구현한 함수가 두번째로 빨랐고, 세 번째로는 미세한 차이로 Equal 함수가 차지했다. 이 둘은 미세하지만 반복하여 실행하여도 순위가 변동되지는 않는다.
네 번째로는 for문으로 loop를 돌면서 직접 값을 비교하도록 구현한 함수가 가장 빨랐고, 그 다음 EqualFold, reflect 패키지의 DeepEqual() 함수 순이였다.
bytes (byte array, slice) 의 값을 비교하는데 있어 결과 값에 대한 비교 처리가 추가로 필요해서 불편하지만 더 나은 방법을 찾기 전까지 당분간은 bytes.Compare() 함수를 이용하게 될 것 같다.
'golang (go)' 카테고리의 다른 글
golang map (0) | 2023.12.24 |
---|---|
golang - const와 iota로 enum(열거)형 구현하기 (1) | 2023.11.18 |
golang type conversion [string(val)], type assertion [val.(string)] (0) | 2023.02.03 |
golang slice or array element(item) remove (delete) (0) | 2023.01.16 |
golang random UUID (0) | 2022.12.04 |
댓글