[Go] 포인터 리시버와 값 리시버

Seyeon_CHOI·2023년 9월 22일
6

Go

목록 보기
1/5
post-thumbnail
💡 Go에서 메서드를 정의할 때, 리시버(receiver)를 사용하여 해당 메서드가 연결될 타입을 지정한다.

리시버(receiver)?

메서드와 연관된 변수를 지칭한다.

리시버는 함수의 첫 번째 매개변수로서 정의되며, 해당 타입에 메서드를 연결하는 역할을 한다.

  • 다른 프로그래밍 언어에서는 "this" 또는 "self"와 유사한 개념이다.
  • 값(value) 또는 포인터(pointer) 형태로 받을 수 있다.
  • 수신자라고도 부른다. (이 글에서는 혼동해서 사용합니다.)

예시)

// func 키워드와 함수 이름 사이의 매개변수

func (s *service) InsertUser(user *entities.User) (*entities.User, error) {
    return s.repository.CreateUser(user)
}



먼저 user 타입을 선언했다고 치자.

type user struct {
	name  string
	email string
}

1. 값 리시버 (Value Receiver)

  • 리시버 타입이 값으로 정의된다.
  • 메서드 내에서는 리시버의 복사본을 사용하므로 원래의 값에는 영향을 주지 않는다.
  • 값 자체에 의미가 있는 기본 타입 (예: int, float64, string 등) 또는 변경 불가능성(immutability)을 원하는 타입에 주로 사용된다.
  • 메서드 호출 시 구조체가 복사되므로 추가적인 메모리 할당이 발생할 수 있다.

예시)

func (u user) notify() {
	fmt.Printf("Sending User Email To %s<%s>\n",
		u.name,
		u.email)
}

notify 메서드의 수신자는 user 타입의 값으로 선언되었다.

이렇게 값 수신자에 정의된 메서드는 호출 시점에 항상 그 값의 복사본을 대상으로 실행된다.

func main() {
	bill := user{"Bill", "bill@email.com"}
	bill.notify()
}

user 타입의 변수 bill을 선언한 후 이름과 메일 주소를 초기화했다.

이 bill 변수를 이용하여 notify 메서드를 호출하고 있다.

변수bill의 값이 메서드 호출을 위한 수신자의 값이 되며, notify메서드는 값의 복사본을 이용해 수행한다.

추가 예시)
포인터를 이용해서 값 수신자에 정의된 메서드를 호출할 수도 있다.

lisa := &user{"Lisa", "lisa@email.com"}
lisa.notify()

user 타입의 변수 lisa를 선언한 후 이름과 메일 주소를 초기화한다.
포인터 변수를 이용하여 notify 메서드를 호출한다.

자세히 설명해보자면,

  1. user{"Lisa", "lisa@email.com"}: user 타입의 새로운 인스턴스를 생성한다.
  2. &user{"Lisa", "lisa@email.com"}: & 연산자를 사용하여 해당 user 인스턴스의 메모리 주소를 가져온다.
  3. lisa := &user{"Lisa", "lisa@email.com"}: 이 주소를 lisa라는 변수에 할당한다. 따라서 lisauser 타입의 포인터이다.

비유하자면,

  • `user{"Lisa", "lisa@email.com"}`는 실제 "집"
  • &user{"Lisa", "lisa@email.com"}는 그 집의 "주소"

lisa 변수는 그 주소를 저장하므로, 언제든지 집을 찾아갈 수 있다.

이때 메서드 호출이 가능하도록 하기 위해 Go는 포인터 값을 메서드의 수신자에 적합한 값으로 조정한다.

(*lisa).notify()

포인터 값을 역참조하여 값 수신자에 정의된 메서드를 호출할 수 있도록 돕는다.

2. 포인터 리시버 (Pointer Receiver)

  • 리시버 타입이 포인터로 정의된다.
  • 원래의 값에 대한 변경이 가능하다. 즉, 메서드 내에서 리시버가 가리키는 값에 대한 변경이 원래의 변수에 반영된다.
  • 일반적으로 구조체의 필드를 수정하거나, 큰 구조체에서 메모리 효율성을 높이기 위해 사용된다.
  • 메서드 호출 시 추가적인 메모리 할당이 발생하지 않는다(구조체의 복사가 일어나지 않기 때문).

예시)

func (u *user) changeEmail(email string) {
	u.email = email
}

포인터 수신자에 정의한 메서드를 호출하면 메서드를 호출하기 위해 사용된 값을 메서드가 공유한다.

lisa := &user{"Lisa", "lisa@email.com"}

bill.changeEmail("bill@newdomain.com")

lisa 포인터 값을 선언한 후 changeEmail 메서드를 호출했다.

이 경우 changeEmail 메서드 내에서 lisa 포인터가 가리키는 값에 대해 이루어진 모든 변경 사항은 메서드 호출이 리턴된 이후에도 계속해서 유지된다. (포인터 수신자는 실제 값을 전달받는다.)

→ 이 점이 포인터 수신자의 장점이다.

bill := &user{"Bill", "bill@email.com"}

bill.changeEmail("bill@newdomain.com")

여기서는 bill 변수를 선언한 후 포인터 수신기에 선언된 changeEmail 메서드를 호출한다.

아까도 말했듯, Go는 포인터 값을 메서드의 수신자에 적합한 값으로 알아서 조정한다.

(&bill).changeEamil("bill@newsdomain.com")

이번 경우에는 값을 참조하여 메서드 호출에 적합한 수신자 타입으로 변환한다.

→ Go는 편리하게도 메서드의 본래 수신자와 일치하지 않는 값과 포인터를 이용해도 메서드를 호출할 수 있도록 허용하고 있다.


그래서 포인터 리시버와 값 리시버 중에 뭘 써야 하는데?

새로운 타입을 정의 했다고 가정하자.

이 타입의 값에 무언가를 더하거나 삭제한다면,

  • 새로운 값이 생성되어야 하는가?
  • 기존의 값이 변경되어야 하는가?

이 질문에 대한 답이 새로운 값이라면 메서드에 값 수신자, 기본 값의 변경이 답이라면 포인터 수신자를 사용하자.

따라서, 만약 메서드 내에서 구조체의 상태를 변경하는 작업이 필요한 경우에는 포인터 리시버를 사용하는 것이 좋다. 반대로, 구조체의 상태를 변경하지 않는 읽기 전용 작업을 수행하는 경우에는 값 리시버를 사용하는 것이 좋을 수 있다.

하지만, 메서드가 이 값으로 무엇을 수행하느냐보단 그 값의 본질에 집중하기를 권한다.

참고: Go 인 액션

profile
오물쪼물 코딩생활 ๑•‿•๑

3개의 댓글