🔥 인터페이스 값과 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) hello
text
첫 번째 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 기본 값을 유연하게 처리할 수 있어요. 개발자들은 이를 활용하여 더 견고하고 우아한 코드를 작성할 수 있게 되죠.