🔥 인터페이스 값과 nil 기본 값
Go 언어에서는 인터페이스 안에 실제 값이 nil이더라도 메서드를 nil 수신자로 호출할 수 있어요. 다른 언어에서는 이런 경우 null 포인터 예외가 발생하겠지만, Go에서는 nil 수신자로 호출되었을 때 우아하게 처리하도록 메서드를 작성하는 것이 일반적이에요.
다음 예제 코드를 살펴보면서 자세히 알아보도록 해요.
package main import "fmt" type I interface { M() } type T struct { S string } func (t *T) M() { if t == nil { fmt.Println("<nil>") return } fmt.Println(t.S) } func main() { var i I var t *T i = t describe(i) i.M() i = &T{"hello"} describe(i) i.M() } func describe(i I) { fmt.Printf("(%v, %T)\n", i, i) }go
먼저 I 인터페이스를 정의했어요. 이 인터페이스는 M() 메서드를 가지고 있죠.
type I interface { M() }go
그리고 T 구조체를 정의하고, 이 구조체는 S라는 문자열 필드를 가지고 있어요.
type T struct { S string }go
T 구조체는 I 인터페이스를 구현하기 위해 M() 메서드를 가지고 있죠. 이 메서드는 수신자가 nil인 경우를 우아하게 처리해요.
func (t *T) M() { if t == nil { fmt.Println("<nil>") return } fmt.Println(t.S) }go
- 만약
t가 nil이면,"<nil>"을 출력하고 메서드를 종료해요. - nil이 아니라면
t.S를 출력하죠.
main 함수에서는 인터페이스 값 i를 선언하고, 이를 nil 값과 non-nil 값으로 설정해 보면서 M() 메서드를 호출해 볼 거예요.
func main() { var i I var t *T i = t describe(i) i.M() i = &T{"hello"} describe(i) i.M() }go
- 먼저
i를 nil 값인t로 설정하고,describe(i)와i.M()을 호출해요.describe(i)는i의 값과 타입을 출력하죠.i.M()은 nil 수신자로M()메서드를 호출하게 되고,"<nil>"이 출력될 거예요.
- 그 다음엔
i를 non-nil 값인&T{"hello"}로 설정하고, 다시describe(i)와i.M()을 호출해요.- 이번에는
i가T구조체를 가리키고 있으므로,i.M()은"hello"를 출력할 거예요.
- 이번에는
describe 함수는 인터페이스 값의 실제 값과 타입을 출력해 주는 헬퍼 함수예요.
func describe(i I) { fmt.Printf("(%v, %T)\n", i, i) }go
이 예제를 실행하면 다음과 같은 결과를 볼 수 있을 거예요.
(<nil>, *main.T) <nil> (&{hello}, *main.T) hellotext
첫 번째 describe(i) 호출에서는 i가 nil 값을 가지고 있지만, 타입은 *main.T임을 알 수 있죠. 그리고 i.M() 호출에서는 "<nil>"이 출력되요.
두 번째 describe(i) 호출에서는 i가 &{hello}라는 값을 가지고 있고, 타입은 여전히 *main.T예요. 이번에 i.M() 호출에서는 "hello"가 출력되죠.
이 예제를 통해 우리는 인터페이스 값 내부의 실제 값이 nil이더라도 메서드 호출이 가능하며, nil 수신자를 적절히 처리할 수 있음을 배웠어요. 또한 nil 실제 값을 가지는 인터페이스 값 자체는 nil이 아님을 알 수 있었죠. 이 부분을 좀 더 얘기해 볼게요.
코드의 이 부분을 다시 살펴보면,
var i I var t *T i = t describe(i)go
여기서 t는 *T 타입의 nil 값이에요. 그리고 i는 t로 초기화되죠. 그럼 i는 어떤 값을 가질까요?
describe(i) 호출 결과를 보면,
(<nil>, *main.T)text
i의 값은 <nil>로 표시되지만, 타입은 *main.T로 나와요. 이는 i 자체는 nil이 아니라는 것을 보여주죠.
i는 인터페이스 타입 I의 변수예요. 인터페이스 값은 실제 값(value)과 타입(type) 정보를 모두 가지고 있어요. 여기서 i의 실제 값은 nil이지만, 타입은 *main.T예요.
따라서 "nil 실제 값을 가지는 인터페이스 값 자체는 nil이 아님"이란 말은, i가 nil 값을 가진 *main.T 타입을 가리키고 있지만, i 자체는 nil이 아니라는 것을 의미해요.
이는 다음 코드에서도 확인할 수 있어요.
if i == nil { fmt.Println("i is nil") } else { fmt.Println("i is not nil") }go
이 코드를 실행하면 "i is not nil"이 출력될 거예요. 왜냐하면 i 자체는 nil이 아니기 때문이죠.
이 개념은 인터페이스를 사용할 때 주의해야 할 중요한 부분이에요. 인터페이스 값이 nil인지 확인할 때는, 인터페이스 값 자체가 nil인지 확인해야지, 인터페이스 값이 가리키는 실제 값이 nil인지를 확인하면 안 되죠.
즉, i는 nil 값을 가진 *main.T 타입을 가리키는, nil이 아닌 인터페이스 값이에요. 이렇게 인터페이스 값과 실제 값을 구분하는 것이 중요해요.
이렇게 Go 언어에서는 nil 인터페이스 값과 nil 기본 값을 유연하게 처리할 수 있어요. 개발자들은 이를 활용하여 더 견고하고 우아한 코드를 작성할 수 있게 되죠.