🔥 인터페이스 값과 nil 기본 값

605자
7분

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
  1. 먼저 i를 nil 값인 t로 설정하고, describe(i)i.M()을 호출해요.
    • describe(i)i의 값과 타입을 출력하죠.
    • i.M()은 nil 수신자로 M() 메서드를 호출하게 되고, "<nil>"이 출력될 거예요.
  2. 그 다음엔 i를 non-nil 값인 &T{"hello"}로 설정하고, 다시 describe(i)i.M()을 호출해요.
    • 이번에는 iT 구조체를 가리키고 있으므로, 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 값이에요. 그리고 it로 초기화되죠. 그럼 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 기본 값을 유연하게 처리할 수 있어요. 개발자들은 이를 활용하여 더 견고하고 우아한 코드를 작성할 수 있게 되죠.