메서드 표현식
메서드 표현식
메서드를 값처럼 꺼내서 다루고 싶을 때가 있어요. 예를 들어 T.Mv처럼 타입에 점을 붙여 메서드를 하나의 함수 값으로 만들 수 있는데, 이걸 메서드 표현식(Method expressions) 이라고 해요. 이번에는 이 표현이 어떻게 동작하는지 하나씩 살펴볼게요.
출처: Go Specification
본문
타입 T의 메서드 집합(method set)에 메서드 M이 들어 있다면, T.M은 일반 함수처럼 호출할 수 있는 함수예요. 다만 이 함수는 M이 받는 인자들 앞에, 메서드의 리시버 역할을 하는 인자가 하나 더 붙은 형태로 호출돼요.
이는 EBNF로 이렇게 표현해요.
MethodExpr = ReceiverType "." MethodName .
ReceiverType = Type .
쉽게 감이 오도록, 메서드 두 개를 가진 구조체 타입 T를 생각해 볼게요. Mv는 리시버가 T 타입이고, Mp는 리시버가 *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
이제 이 식을 보면,
T.Mv
이건 Mv와 동일하되, 리시버가 첫 번째 인자로 명시적으로 들어가는 함수가 돼요. 시그니처(signature)는 이렇게 되죠.
func(tv T, a int) int
이 함수는 명시적인 리시버를 첫 인자로 넘겨서 평범하게 호출할 수 있어요. 그래서 아래 다섯 가지의 호출은 전부 같은 결과를 내요.
t.Mv(7)
T.Mv(t, 7)
(T).Mv(t, 7)
f1 := T.Mv; f1(t, 7)
f2 := (T).Mv; f2(t, 7)
마찬가지로 이 식은,
(*T).Mp
Mp를 나타내면서 이런 시그니처를 가진 함수 값을 만들어 내요.
func(tp *T, f float32) float32
값 리시버(value receiver)를 가진 메서드라면 명시적인 포인터 리시버를 가진 함수로도 파생할 수 있어요. 그래서
(*T).Mv
이건 Mv를 나타내면서 이런 시그니처를 가진 함수 값을 만들어 내요.
func(tv *T, a int) int
이런 함수는 리시버를 통해 간접 참조(indirect)해서, 내부 메서드에 리시버로 넘겨줄 값을 하나 만들어 내요. 이때 메서드는 함수 호출에서 주소가 전달된 그 값을 덮어쓰지 않아요.
마지막 경우, 포인터 리시버 메서드를 위한 값 리시버 함수를 만드는 것은 불법이에요. 포인터 리시버 메서드는 값 타입의 메서드 집합에 들어 있지 않기 때문이죠.
메서드에서 파생된 함수 값은 함수 호출 문법으로 호출돼요. 리시버는 호출의 첫 번째 인자로 제공돼요. 즉 f := T.Mv가 주어지면 f는 t.f(7)이 아니라 f(t, 7)처럼 호출해요. 리시버를 묶어 둔 함수를 만들고 싶다면 함수 리터럴(function literal)이나 메서드 값(method value)을 사용해요.
인터페이스 타입의 메서드에서 함수 값을 파생하는 것도 합법이에요. 그렇게 만든 함수는 해당 인터페이스 타입을 명시적 리시버로 받아요.
더 알아보기
- 메서드 집합(Method sets) — 메서드 표현식이 무엇을 꺼낼 수 있는지 결정하는 기준
- 메서드 값(Method values) — 리시버를 미리 묶어 두는 표현
- 함수 리터럴(Function literals) — 리시버를 바인딩하는 또 다른 방법