이 글은 Effective Go를 일부 번역한 글입니다.


메서드

포인터 vs. 값

ByteSize에서 봤듯이 이름 지은 유형(Named Type)이라면 아무 유형이나 (포인터나 인터페이스는 예외) 메서드를 정의할 수 있다. 수신자가 구조체일 필요는 없다.

전에 조각을 이야기할 때 Append 함수를 작성했다. 이 함수를 조각의 메서드로 대신 정의할 수 있다. 그러려면 우선 메서드를 연결할 이름 지은 유형을 선언하고, 메서드의 수신자를 해당 유형 값으로 만들어야 한다.

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // Body exactly the same as above
}

이 메서드도 여전히 갱신된 조각을 반환해야 한다. 메서드를 다시 정의하면 이런 지저분한 걸 없앨 수 있다. 메서드의 수신자를 ByteSlice 포인터로 바꾸면 된다. 그러면 메서드가 호출자의 조각을 덮어쓸 수 있다.

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Body as above, without the return.
    *p = slice
}

더 개선해보자. 이 함수를 아래처럼 표준 Write 메서드에 맞게 수정하면 어떨까?

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Again as above.
    *p = slice
    return len(data), nil
}

이러면 *ByteSlice 형이 편리한 표준 인터페이스 io.Writer를 만족한다. 예를 들면, 여기에 출력할 수도 있다.

    var b ByteSlice
    fmt.Fprintf(&b, "This hour has %d days\n", 7)

여기서 ByteSlice의 주소를 넘겼다. io.Writer*ByteSlice만 만족하기 때문이다. 포인터 vs. 값과 관련된 규칙은 이렇다. 값 메서드는 포인터와 값에 적용할 수 있지만, 포인터 메서드는 포인터에만 적용할 수 있다.

이 규칙은 포인터 메서드가 수신자를 고칠 수 있기에 만들었다. 값에 포인터 메서드를 적용하면 메서드가 값의 사본을 받을 것이고, 그러면 수정한 모든 게 사라질 것이기 때문이다. 언어는 따라서 이런 실수를 받아들이지 않는다. 그렇지만 편리한 예외가 있다. 값에 주소가 있으면 언어가 자동으로 주소 연산자를 삽입해 흔한 포인터 메서드 적용으로 처리한다. 위 예제에서 변수 b는 주소가 있고, 따라서 Write 메서드를 b.Write만으로 호출할 수 있다. 컴파일러가 이를 (&b).Write로 바꿀 것이다.

덧붙여, 바이트 조각에 Write를 사용하는 건 bytes.Buffer 구현의 핵심이다.