Go 언어에서 `==` 연산자를 잘못 사용할 수 있는 이유
Wenhao Wang
Dev Intern · Leapcell

Go 언어에서 ==
연산자를 잘못 사용할 수 있는 이유
Go 언어에서 ==
연산의 심층 분석
개요
Go 언어 프로그래밍 실습에서 ==
동등 연산은 매우 일반적입니다. 그러나 포럼에서 소통하다 보면 많은 개발자가 Go 언어에서 ==
연산의 결과에 대해 혼란스러워하는 것을 종종 발견합니다. 실제로 Go 언어가 ==
연산을 처리할 때 특별한 주의가 필요한 많은 세부 사항이 있습니다. 이러한 세부적인 문제는 일상적인 개발에서 덜 자주 발생할 수 있지만, 일단 발생하면 심각한 프로그램 오류로 이어질 수 있습니다. 이 기사에서는 Go 언어에서 ==
연산과 관련된 내용을 체계적이고 심층적으로 설명하여 대부분의 개발자에게 강력한 지원을 제공하고자 합니다.
타입 시스템
Go 언어의 데이터 타입은 다음 네 가지 범주로 나눌 수 있습니다.
- 기본 타입: 정수 타입(
int
,uint
,int8
,uint8
,int16
,uint16
,int32
,uint32
,int64
,uint64
,byte
,rune
등), 부동 소수점 숫자(float32
,float64
), 복소수 타입(complex64
,complex128
), 문자열(string
)을 포함합니다. - 복합 타입 (Aggregate Types): 주로 배열과 구조체 타입을 포함합니다.
- 참조 타입: 슬라이스(
slice
),map
, 채널, 포인터를 포함합니다. - 인터페이스 타입:
error
인터페이스와 같습니다.
==
연산의 주요 전제 조건은 두 피연산자의 타입이 정확히 동일해야 한다는 점을 강조해야 합니다. 타입이 다르면 컴파일 오류가 발생합니다.
다음 사항에 유의해야 합니다.
- Go 언어는 엄격한 타입 시스템을 가지고 있으며 C/C++ 언어와 같은 암시적 타입 변환 메커니즘이 없습니다. 코드를 작성할 때 약간 번거로울 수 있지만 나중에 많은 잠재적 오류를 효과적으로 방지할 수 있습니다.
- Go 언어에서는
type
키워드를 통해 새 타입을 정의할 수 있습니다. 새로 정의된 타입은 기본 타입과 다르며 직접 비교할 수 없습니다.
타입을 더 명확하게 표시하기 위해 샘플 코드의 변수 정의는 모두 타입을 명시적으로 지정합니다. 예를 들면 다음과 같습니다.
package main import "fmt" func main() { var a int8 var b int16 // 컴파일 오류: invalid operation a == b (mismatched types int8 and int16) fmt.Println(a == b) }
이 코드에서 a
와 b
의 타입이 다르기 때문에 (int8
및 int16
각각) ==
비교를 시도하면 컴파일 오류가 발생합니다.
다른 예:
package main import "fmt" func main() { type int8 myint8 var a int8 var b myint8 // 컴파일 오류: invalid operation a == b (mismatched types int8 and myint8) fmt.Println(a == b) }
여기서 myint8
의 기본 타입은 int8
이지만 서로 다른 타입에 속하며 직접 비교하면 컴파일 오류가 발생합니다.
다른 타입에서 ==
연산의 특정 동작
기본 타입
기본 타입의 비교 연산은 비교적 간단하고 명확하며 값이 같은지 여부만 비교합니다. 예는 다음과 같습니다.
var a uint32 = 10 var b uint32 = 20 var c uint32 = 10 fmt.Println(a == b) // false fmt.Println(a == c) // true
그러나 부동 소수점 숫자 비교를 처리할 때는 특별한 주의를 기울여야 합니다.
var a float64 = 0.1 var b float64 = 0.2 var c float64 = 0.3 fmt.Println(a + b == c) // false
이는 컴퓨터에서 일부 부동 소수점 숫자를 정확하게 표현할 수 없으며 부동 소수점 연산 결과에 특정 오류가 발생하기 때문입니다. a + b
와 c
값을 각각 출력하여 차이점을 명확하게 확인할 수 있습니다.
fmt.Println(a + b) fmt.Println(c) // 0.30000000000000004 // 0.3
이 문제는 Go 언어에만 있는 것이 아닙니다. IEEE 754 표준을 따르는 프로그래밍 언어는 부동 소수점 숫자를 처리할 때 유사한 상황에 직면할 수 있습니다. 따라서 프로그래밍에서 직접적인 부동 소수점 숫자 비교는 가능한 한 피해야 합니다. 비교가 정말 필요한 경우 두 부동 소수점 숫자 간의 차이의 절대값을 계산할 수 있습니다. 이 값이 설정된 매우 작은 값(예: 1e - 9
)보다 작으면 같다고 간주할 수 있습니다.
복합 타입
Go 언어의 복합 타입(즉, Aggregate Types)은 배열과 구조체뿐입니다. 복합 타입의 경우 ==
연산은 요소별/필드별로 비교합니다.
배열의 길이는 타입의 일부라는 점에 유의해야 합니다. 길이가 다른 두 배열은 서로 다른 타입에 속하며 직접 비교할 수 없습니다.
배열의 경우 각 요소의 값이 차례로 비교됩니다. 요소의 타입(기본 타입, 복합 타입, 참조 타입 또는 인터페이스 타입일 수 있음)에 따라 해당 타입 비교 규칙에 따라 비교가 판단됩니다. 모든 요소가 동일한 경우에만 두 배열이 동일한 것으로 간주됩니다.
구조체의 경우 각 필드의 값도 차례로 비교됩니다. 필드 타입이 속하는 네 가지 주요 타입 범주에 따라 특정 타입 비교 규칙을 따릅니다. 모든 필드가 동일한 경우에만 두 구조체가 동일합니다.
예는 다음과 같습니다.
a := [4]int{1, 2, 3, 4} b := [4]int{1, 2, 3, 4} c := [4]int{1, 3, 4, 5} fmt.Println(a == b) // true fmt.Println(a == c) // false type A struct { a int b string } aa := A { a : 1, b : "leapcell_test1" } bb := A { a : 1, b : "leapcell_test2" } cc := A { a : 1, b : "leapcell_test3" } fmt.Println(aa == bb) fmt.Println(aa == cc)
참조 타입
참조 타입은 간접적으로 참조하는 데이터를 가리키고 변수는 데이터의 주소를 저장합니다. 따라서 참조 타입의 ==
비교는 실제로 두 변수가 동일한 데이터를 가리키는지 여부를 판단하며, 가리키는 실제 데이터 내용을 비교하는 것이 아닙니다.
예는 다음과 같습니다.
type A struct { a int b string } aa := &A { a : 1, b : "leapcell_test1" } bb := &A { a : 1, b : "leapcell_test1" } cc := aa fmt.Println(aa == bb) fmt.Println(aa == cc)
이 예에서 aa
와 bb
가 가리키는 구조체 값은 동일하지만(위의 복합 타입의 비교 규칙 참조) 서로 다른 구조체 인스턴스를 가리키므로 aa == bb
는 false
입니다. 반면 aa
와 cc
는 동일한 구조체를 가리키므로 aa == cc
는 true
입니다.
channel
을 예로 들어보겠습니다.
ch1 := make(chan int, 1) ch2 := make(chan int, 1) ch3 := ch1 fmt.Println(ch1 == ch2) fmt.Println(ch1 == ch3)
ch1
과 ch2
의 타입은 동일하지만 서로 다른 channel
인스턴스를 가리키므로 ch1 == ch2
는 false
입니다. ch1
과 ch3
은 동일한 channel
을 가리키므로 ch1 == ch3
는 true
입니다.
참조 타입에 대한 두 가지 특별한 규정이 있습니다.
- 슬라이스는 직접 비교할 수 없습니다. 슬라이스는
nil
값과만 비교할 수 있습니다. - 맵은 직접 비교할 수 없습니다. 맵은
nil
값과만 비교할 수 있습니다.
슬라이스를 직접 비교할 수 없는 이유는 다음과 같습니다. 참조 타입인 슬라이스는 간접적으로 자체를 가리킬 수 있습니다. 예를 들어:
a := []interface{}{ 1, 2.0 } a[1] = a fmt.Println(a) // !!! // runtime: goroutine stack exceeds 1000000000 - byte limit // fatal error: stack overflow
위의 코드는 a
를 a[1]
에 할당하여 재귀 참조를 발생시키고 fmt.Println(a)
문을 실행할 때 스택 오버플로 오류가 발생합니다. 슬라이스의 참조 주소를 직접 비교하면 한편으로는 배열의 비교 방법과 매우 다르며 개발자를 혼란스럽게 할 수 있습니다. 다른 한편으로는 슬라이스의 길이와 용량은 타입의 일부이며 길이가 다른 슬라이스에 대한 통합 비교 규칙을 결정하기 어렵습니다. 배열처럼 슬라이스 내부의 요소를 비교하면 순환 참조 문제가 발생합니다. 이 문제는 언어 수준에서 해결할 수 있지만 Go 언어 개발 팀은 이에 너무 많은 노력을 투자할 가치가 없다고 생각합니다. 위의 이유로 Go 언어는 슬라이스 타입을 직접 비교할 수 없다고 명확하게 규정하고 있으며, ==
를 사용하여 슬라이스를 비교하면 컴파일 오류가 발생합니다. 예를 들어:
var a []int var b []int // invalid operation: a == b (slice can only be compared to nil) fmt.Println(a == b)
오류 메시지는 슬라이스를 nil
값과만 비교할 수 있음을 명확하게 나타냅니다.
map
타입의 경우 값 타입이 비교할 수 없는 타입(예: 슬라이스)일 수 있으므로 map
타입도 직접 비교할 수 없습니다.
인터페이스 타입
인터페이스 타입은 Go 언어에서 중요한 역할을 합니다. 인터페이스 타입의 값, 즉 인터페이스 값은 두 부분으로 구성됩니다. 특정 타입(즉, 인터페이스에 저장된 값의 타입)과 해당 타입의 값입니다. 참조 용어로는 동적 타입과 동적 값이라고 합니다. 인터페이스 값의 비교에는 이러한 두 부분의 비교가 포함됩니다. 동적 타입이 정확히 동일하고 동적 값이 동일한 경우에만(동적 값은 ==
를 사용하여 비교) 두 인터페이스 값이 동일합니다.
예는 다음과 같습니다.
var a interface{} = 1 var b interface{} = 1 var c interface{} = 2 var d interface{} = 1.0 fmt.Println(a == b) // false fmt.Println(a == c) // true fmt.Println(a == d) // false
이 예에서 a
와 b
의 동적 타입은 동일하고(둘 다 int
), 동적 값도 동일하므로(둘 다 1
이며 기본 타입의 비교에 속함) a == b
는 true
입니다. a
와 c
의 동적 타입은 동일하지만 동적 값은 동일하지 않으므로(1
과 2
각각) a == c
는 false
입니다. a
와 d
의 동적 타입은 다르므로(a
는 int
이고 d
는 float64
) a == d
는 false
입니다.
구조체가 인터페이스 값으로 사용되는 상황을 살펴보겠습니다.
type A struct { a int b string } var aa interface{} = A { a: 1, b: "test" } var bb interface{} = A { a: 1, b: "test" } var cc interface{} = A { a: 2, b: "test" } fmt.Println(aa == bb) // true fmt.Println(aa == cc) // false var dd interface{} = &A { a: 1, b: "test" } var ee interface{} = &A { a: 1, b: "test" } fmt.Println(dd == ee) // false
aa
와 bb
의 동적 타입은 동일하고(둘 다 A
), 동적 값도 동일하므로(위의 복합 타입에서 구조체의 비교 규칙에 따라) aa == bb
는 true
입니다. aa
와 cc
의 동적 타입은 동일하지만 동적 값은 다르므로 aa == cc
는 false
입니다. dd
와 ee
의 동적 타입은 동일하고(둘 다 *A
) 동적 값은 포인터(참조) 타입의 비교 규칙을 사용합니다. 동일한 주소를 가리키지 않으므로 dd == ee
는 false
입니다.
인터페이스의 동적 값이 비교할 수 없는 경우 억지로 비교하면 panic
이 발생한다는 점에 유의해야 합니다. 예를 들어:
var a interface{} = []int{1, 2, 3, 4} var b interface{} = []int{1, 2, 3, 4} // panic: runtime error: comparing uncomparable type []int fmt.Println(a == b)
여기서 a
와 b
의 동적 값은 슬라이스 타입이고 슬라이스 타입은 비교할 수 없으므로 a == b
를 실행하면 panic
이 발생합니다.
또한 인터페이스 값의 비교는 인터페이스 타입(동적 타입이 아님)이 정확히 동일할 필요는 없습니다. 하나의 인터페이스를 다른 인터페이스로 변환할 수 있는 한 비교를 수행할 수 있습니다. 예를 들어:
var f *os.File var r io.Reader = f var rc io.ReadCloser = f fmt.Println(r == rc) // true var w io.Writer = f // invalid operation: r == w (mismatched types io.Reader and io.Writer) fmt.Println(r == w)
r
의 타입은 io.Reader
인터페이스이고 rc
의 타입은 io.ReadCloser
인터페이스입니다. 소스 코드를 살펴보면 io.ReadCloser
의 정의는 다음과 같습니다.
type ReadCloser interface { Reader Closer }
io.ReadCloser
를 io.Reader
로 변환할 수 있으므로 r
과 rc
를 비교할 수 있습니다. 반면 io.Writer
는 io.Reader
로 변환할 수 없으므로 컴파일 오류가 발생합니다.
type
으로 정의된 타입
type
키워드를 통해 기존 타입을 기반으로 정의된 새 타입의 경우 비교는 기본 타입에 따라 수행됩니다. 예를 들어:
type myint int var a myint = 10 var b myint = 20 var c myint = 10 fmt.Println(a == b) // false fmt.Println(a == c) // true type arr4 [4]int var aa arr4 = [4]int{1, 2, 3, 4} var bb arr4 = [4]int{1, 2, 3, 4} var cc arr4 = [4]int{1, 2, 3, 5} fmt.Println(aa == bb) fmt.Println(aa == cc)
여기서 myint
타입은 기본 타입 int
에 따라 비교되고 arr4
타입은 기본 타입 [4]int
에 따라 비교됩니다.
비교 불가능성 및 영향
위에서 언급했듯이 Go 언어의 슬라이스 타입은 비교할 수 없습니다. 이로 인한 영향은 슬라이스를 포함하는 모든 타입도 비교할 수 없다는 것입니다. 구체적으로는 다음을 포함합니다.
- 배열 요소가 슬라이스 타입입니다.
- 구조체에 슬라이스 타입의 필드가 포함됩니다.
- 포인터가 슬라이스 타입을 가리킵니다.
비교 불가능성은 전이적입니다. 구조체가 슬라이스 필드를 포함하기 때문에 비교할 수 없는 경우 해당 요소를 가진 배열은 비교할 수 없으며 해당 필드 타입을 가진 구조체도 비교할 수 없습니다.
map
과 비교할 수 없는 타입 간의 관계
map
의 키-값 쌍은 동등성 판단을 위해 ==
연산을 사용하므로 비교할 수 없는 모든 타입은 map
의 키로 사용할 수 없습니다. 예를 들어:
// invalid map key type []int m1 := make(map[[]int]int) type A struct { a []int b string } // invalid map key type A m2 := make(map[A]int)
위의 코드에서 슬라이스 타입은 비교할 수 없으므로 m1 := make(map[[]int]int)
는 컴파일 오류를 보고합니다. 구조체 A
는 슬라이스 필드를 포함하기 때문에 비교할 수 없으므로 m2 := make(map[A]int)
도 컴파일 오류를 보고합니다.
결론
이 기사에서는 Go 언어에서 ==
연산의 세부 사항을 포괄적이고 심층적으로 소개하여 다양한 데이터 타입에서 ==
연산의 동작, 특수 타입의 비교 규칙 및 비교할 수 없는 타입으로 인한 영향을 다루었습니다. 이 기사의 설명을 통해 대부분의 개발자가 Go 언어에서 ==
연산을 더 정확하고 깊이 이해하고 적용하며 실제 프로그래밍에서 이에 대한 불충분한 이해로 인해 발생하는 다양한 문제를 피할 수 있기를 바랍니다.
Leapcell: 최고의 서버리스 웹 호스팅
마지막으로 Go 서비스를 배포하는 데 가장 적합한 플랫폼인 **Leapcell**을 추천합니다.
🚀 좋아하는 언어로 빌드
JavaScript, Python, Go 또는 Rust로 쉽게 개발하세요.
🌍 무제한 프로젝트를 무료로 배포
사용한 만큼만 지불하세요. 요청이나 요금이 없습니다.
⚡ 사용량에 따라 지불, 숨겨진 비용 없음
유휴 요금이 없으며 원활한 확장성만 제공됩니다.
🔹 Twitter에서 팔로우하세요: @LeapcellHQ