메서드 값

메서드 값 (Method values)

메서드를 호출하는 방법은 t.M(7)처럼 직접 부르는 것만 있는 게 아니에요. t.M처럼 메서드를 값으로 떼어내서 나중에 함수처럼 호출할 수도 있는데, 이걸 Go에서는 **메서드 값(method value)**이라고 불러요. 함수 값이 하나 생겼다고 생각하면 돼요.

출처: Go Specification

본문

표현식 x의 정적 타입이 T이고, M이 타입 T메서드 집합(method set)에 속해 있다고 해볼게요. 그럼 x.M을 **메서드 값(method value)**이라고 불러요. 이 메서드 값 x.M은 함수 값인데, x.M을 메서드로 호출할 때와 똑같은 인자들로 호출할 수 있는 함수예요.

여기서 중요한 점이 하나 있어요. 표현식 x는 메서드 값을 평가하는 동안 한 번 평가되어 저장되고, 그 저장된 복사본이 이후의 모든 호출에서 리시버로 쓰여요. 그 호출은 나중에 실행될 수도 있죠. 즉 메서드 값을 만드는 순간의 리시버가 고정된다는 뜻이에요.

type S struct { *T }
type T int
func (t T) M() { print(t) }

t := new(T)
s := S{T: t}
f := t.M                    // receiver *t is evaluated and stored in f
g := s.M                    // receiver *(s.T) is evaluated and stored in g
*t = 42                     // does not affect stored receivers in f and g

이 예시에서 f에는 *t가, g에는 *(s.T)가 각각 평가되어 저장돼요. 그래서 나중에 *t = 42로 값을 바꿔도 이미 저장된 리시버에는 영향이 없어요. 타입 T는 인터페이스 타입일 수도 있고, 인터페이스가 아닌 타입일 수도 있어요.

그럼 메서드 값이 실제로 어떤 타입의 함수 값이 되는지, 앞서 다룬 메서드 표현식(method expression)과 비교해 보면서 확인해 볼게요. 리시버가 각각 T*T인 두 메서드를 가진 구조체 타입 T를 생각해 보죠.

type T struct {
	a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

var t T
var pt *T
func makeT() T

먼저 표현식 t.Mv를 보면, 이건 func(int) int 타입의 함수 값을 만들어요. 그래서 아래 두 호출은 완전히 동등해요.

t.Mv(7)
f := t.Mv; f(7)

마찬가지로 표현식 pt.Mp를 보면, 이건 func(float32) float32 타입의 함수 값을 만들어요.

pt.Mp

이제 셀렉터(selector)에서 본 것과 마찬가지로, 값 리시버를 가진 비인터페이스 메서드를 포인터로 참조하면 그 포인터가 자동으로 역참조(dereference)돼요. 그래서 pt.Mv(*pt).Mv와 동등하죠.

반대로 메서드 호출(method call)에서처럼, 포인터 리시버를 가진 비인터페이스 메서드를 주소 지정이 가능한 값으로 참조하면 그 값의 주소가 자동으로 취해져요. 그래서 t.Mp(&t).Mp와 동등해요.

f := t.Mv; f(7)   // like t.Mv(7)
f := pt.Mp; f(7)  // like pt.Mp(7)
f := pt.Mv; f(7)  // like (*pt).Mv(7)
f := t.Mp; f(7)   // like (&t).Mp(7)
f := makeT().Mp   // invalid: result of makeT() is not addressable

마지막 줄이 왜 오류일까요? makeT()의 결과는 값이지 주소를 얻을 수 있는(addressable) 대상이 아니거든요. 포인터 리시버 메서드는 자동으로 주소를 취하려 하는데, 주소를 취할 수 없는 값이라서 오류가 나는 거예요.

지금까지 예시는 전부 비인터페이스 타입이었는데요, 인터페이스 타입의 값에서도 메서드 값을 만드는 건 합법적이에요.

var i interface { M(int) } = myVal
f := i.M; f(7)  // like i.M(7)

더 알아보기