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

