메서드 선언하기
메서드는 리시버가 있는 함수입니다. 리시버는 메서드 이름 앞에 쓰는 추가 매개변수이며, 메서드를 타입에 묶어 줍니다.
리시버 이름은 보통 타입에서 따온 한두 글자입니다(Circle이면 c). Go는 this나 self를 쓰지 않으며, 스타일 가이드도 그런 이름을 권하지 않습니다.
Circle에는 String() string 메서드가 있으므로 fmt.Stringer를 만족하고, fmt.Println은 {2} 대신 그 결과를 출력합니다.
값 리시버와 포인터 리시버
값 리시버 (c Circle)는 복사본을 받습니다. 포인터 리시버 (c *Circle)는 주소를 받으므로 필드를 바꾸면 그대로 남습니다.
IncByValue는 컴파일되지만 눈에 보이는 일은 아무것도 하지 않습니다. 전형적인 버그입니다. 리시버를 수정하려는 메서드라면 포인터 리시버가 필요합니다.
이 호출들은 Go가 주는 두 가지 편의도 보여 줍니다. 주소를 얻을 수 있는 변수에 대한 c.Inc()는 (&c).Inc()로 바뀌고, 포인터에 대한 p.Value()는 (*p).Value()로 바뀝니다. 메서드를 호출하려고 &나 *를 직접 쓸 일은 거의 없습니다.
자동 &에는 주소를 얻을 수 있는 대상이 필요합니다. 맵 요소나 복합 리터럴은 그렇지 않습니다:
m := map[string]Counter{"a": {}}
m["a"].Inc() // compile error: cannot call pointer method Inc on Counter
Counter{}.Inc() // same error: a composite literal is not addressable
맵에 포인터를 저장하거나(map[string]*Counter), 값을 꺼내서 바꾼 뒤 다시 써 넣으세요.
어떤 리시버를 고를까
| 포인터 리시버를 쓸 때 | 값 리시버를 쓸 때 |
|---|---|
| 메서드가 리시버를 수정함 | 타입이 작고 메서드가 절대 수정하지 않음(time.Time, 좌표) |
| 구조체가 큼(복사 비용) | 타입이 맵, 함수, 채널임(이미 참조) |
타입이 sync.Mutex 같은 것을 담음 | 값이 기본 타입처럼 동작하고 복사해도 안전하길 원함 |
| 타입의 다른 메서드가 이미 포인터를 씀 |
Go FAQ와 표준 라이브러리는 한 가지 경험 법칙을 따릅니다. 일관성을 지키라는 것입니다. 포인터 리시버가 필요한 메서드가 하나라도 있다면 모든 메서드를 포인터 리시버로 만들어서 메서드 집합을 예측할 수 있게 하세요. go vet은 sync.Mutex를 담은 구조체가 값 리시버로 복사되는 것을 지적합니다(passes lock by value).
메서드 집합과 인터페이스
이 구분이 가장 중요해지는 곳은 인터페이스입니다. T의 메서드 집합에는 값 리시버 메서드가 들어 있습니다. *T의 메서드 집합에는 값 리시버 메서드와 포인터 리시버 메서드가 모두 들어 있습니다.
그 슬라이스에 & 없이 Square{Side: 3}라고 쓰면 컴파일에 실패합니다:
cannot use Square{…} (value of struct type Square) as Shape value in array or slice literal: Square does not implement Shape (method Area has pointer receiver)
이유는 이렇습니다. 인터페이스는 주소를 얻을 수 없는 Square의 복사본을 담을 수 있으므로, Go는 포인터 메서드를 호출하려고 그 주소를 얻을 수 없습니다. 나머지 이야기는 인터페이스를 참고하세요.
구조체가 아닌 타입의 메서드
구조체뿐 아니라 패키지에서 선언한 이름 있는 타입이라면 무엇이든 메서드를 가질 수 있습니다.
제약은 이렇습니다. 타입은 메서드와 같은 패키지에서 정의되어야 하고, 기반 타입이 포인터나 인터페이스여서는 안 됩니다. 자기 패키지에서 int, string, time.Time에 메서드를 추가할 수는 없습니다. 새 이름 있는 타입으로 감싸는 것이 Go의 방식입니다. 정수 타입의 String()은 열거형이 읽기 좋은 출력을 내는 방법이기도 합니다. iota로 만드는 열거형을 참고하세요.
메서드 값과 메서드 표현식
특정 리시버에 묶인 메서드는 함수 값입니다:
값 리시버의 메서드 값은 만들어질 때 리시버를 복사합니다. 그래서 f는 여전히 Ana라고 말합니다. 포인터 리시버라면 포인터를 저장하므로 이후의 변경을 봅니다. 메서드 값은 콜백에 편리합니다: http.HandleFunc("/", srv.handleIndex).
흔한 실수
- 수정하는 메서드에 값 리시버를 씀. 컴파일되지만 조용히 복사본만 바꿉니다.
- 리시버 종류를 섞어 씀.
T와*T중 무엇이 인터페이스를 만족하는지 불분명해집니다. 타입마다 하나로 정하세요. - nil 포인터에서 포인터 메서드를 호출함. 허용되며, 메서드는 nil 리시버로 실행됩니다. 필드에 접근할 때만 패닉이 납니다. 일부 타입(nil에 안전한 연결 리스트 등)은 이것을 일부러 활용하지만, 대부분은 예상하지 않습니다.
자주 묻는 질문
Go에서 메서드와 함수의 차이는 무엇인가요?
메서드에는 func와 이름 사이에 쓰는 리시버가 있습니다: func (c Circle) Area() float64. 값에 대해 호출하고(c.Area()), 타입의 메서드 집합에 속하며, 타입이 인터페이스를 만족하는 수단입니다. 그 외에는 첫 번째 인자가 리시버인 함수처럼 동작합니다.
Go에서 포인터 리시버는 언제 써야 하나요?
메서드가 리시버를 수정해야 할 때, 구조체가 커서 호출할 때마다 복사하는 것이 낭비일 때, 또는 sync.Mutex처럼 복사하면 안 되는 것을 타입이 담고 있을 때 포인터 리시버를 씁니다. 타입의 메서드 중 하나라도 포인터 리시버가 필요하다면, 일관성을 위해 모든 메서드를 포인터 리시버로 만드는 것이 일반적인 조언입니다.
메서드가 포인터 리시버일 때 내 타입이 인터페이스를 구현하지 못하는 이유는 무엇인가요?
포인터 리시버 메서드는 T가 아니라 *T의 메서드 집합에 속합니다. 그래서 var s Shape = Square{}는 Square does not implement Shape (method Area has pointer receiver)로 실패합니다. 대신 포인터를 대입하세요: var s Shape = &Square{}.
Go에서 내장 타입에 메서드를 정의할 수 있나요?
직접은 안 됩니다. 메서드는 같은 패키지에서 정의한 이름 있는 타입에만 선언할 수 있습니다. 먼저 type Celsius float64처럼 자신만의 타입을 정의한 뒤 메서드를 추가하세요. int, string, 다른 패키지의 타입에는 메서드를 추가할 수 없습니다.