home

Go에서 값의 본질이란

go의 메서드에는 두 가지 수신자를 사용할 수 있습니다. 값 수신자와 포인터 수신자인데요. go에 대해서 대충알고 있을 때 과연 어떤 상황에서 어떤 수신자를 써야할까 고민이 깊었습니다. 그런데 책에서 이렇게 설명하더군요.

사실 메서드를 정의할 때 값 수신자와 포인터 수신자 중 어느 것을 사용할 것인지를 선택하는데 메서드가 수신된 값을 변경하는지는 전혀 관련이 없다. 둘 중 어느 것을 선택할 것인지는 값의 본질에 따라 결정해야 한다.

즉, 중요한 것은 ‘값의 본질’이 무엇인지 이해하는 것인데요, 같은 구초제라도 값 타입과 참조 타입으로 나눌 수 있다는 것입니다.

값 타입이라고 하면 그 자체로 완전한 의미를 갖는 데이터 라고 볼 수 있습니다. 예를 들면

  • 숫자, 문자열, 시간, 좌표 등 한번 정해지면 변하지 않는 것
  • 복사해도 원본과 동일한 의미를 가지는 것
  • 불변성이라는 특성이 자연스러운 것

으로 생각해볼 수 있습니다.

반대로 참조 타입이라고 하면 어떤 리소스나 상태를 가리키는, 혹은 관리하는 데이터라고 볼 수 있는데요, 예를 들면

  • 파일 핸들 객체, 데이터베이스 연결 객체, 뮤텍스 등
  • 복사하면 의미가 달라지거나 문제가 발생할 수 있는 것
  • 가변성이라는 특성이 자연스러운 것

라고 정의를 해볼 수 있습니다.

예시를 들어보겠습니다. 실제 golang의 Time 구조체는 시간이라는 값 타입의 특성을 갖기 때문에, 다음과 같이 정의되어있습니다.

type Time struct {
    wall uint64
    ext  int64
    loc  *Location
}

// 값 수신자 사용
func (t Time) Add(d Duration) Time {
    return Time{...} // 새로운 Time 반환
}

func (t Time) Format(layout string) string {
    // t를 변경하지 않고 문자열 반환
}

“2023년 1월 1일 12시”라는 시간 자체가 완전한 의미이고, 복사를 해도 새로운 값이 아닌 같은 시간을 나타냅니다. 즉, 시간을 수정한다는 것은 새로운 시간을 만드는 것이죠.

반면 File 구조체는 참조의 본질을 가집니다.

type File struct {
    *file // 내부 파일 상태
}

// 포인터 수신자 사용
func (f *File) Read(b []byte) (n int, err error) {
    // 파일의 읽기 위치 등 내부 상태 변경
}

func (f *File) Close() error {
    // 파일 핸들 해제
}

여기서 파일 객체는 실제 파일 리소르를 가르기코, 복사를 하면 같은 파일을 가르키는 여러 핸들러가 생성이 됩니다. 즉 여러 핸들러 각자의 파일 작업은 실제 리소스의 상태를 변경하게 되는 것이죠.

여기서 중요한 것이 저 *file 입니다. 이것을 통해서 특정 타입에 대한 간접적인 접근을 허용함으로써 값이 복사되는 것을 방지할 수가 있는데, 이를 타입 임베딩이라고 합니다.