Go 언어에서 메서드를 정의할 때, 구조체와 메서드의 관계를 결정하는 중요한 요소 중 하나는 리시버(Receiver)의 타입임
리시버는 메서드가 호출될 때 어떤 방식으로 구조체 데이터를 다룰지 결정하며, 값 리시버와 포인터 리시버 두 가지 방식이 있음
값 리시버란?
값 리시버는 메서드 호출 시, 구조체의 복사본을 리시버로 받는 방식
즉, 메서드가 호출될 때 원본 구조체가 아닌, 그 복사된 값이 메서드로 전달됨
package main
import "fmt"
type Counter struct {
count int
}
func (c Counter) Increment() {
c.count++
}
func (c Counter) GetCount() int {
return c.count
}
func main() {
counter := Counter{}
counter.Increment()
fmt.Println(counter.GetCount()) // 출력: 0
}
위 코드에서 Increment() 메서드는 값 리시버를 사용함
메서드가 호출될 때 Counter 구조체의 복사본이 전달되기 때문에, Increment()가 count 값을 증가시키더라도 원본 구조체의 값은 변하지 않음
따라서 GetCount() 메서드로 값을 확인하면 여전히 0이 출력됨
Increment 메서드는 아무런 의미가 없는 메서드라고 볼 수 있음
포인터 리시버란?
포인터 리시버는 메서드 호출 시, 구조체의 메모리 주소를 전달받아 원본 데이터를 직접 수정할 수 있는 방식
포인터 리시버를 사용하면 메서드 내부에서 구조체의 상태를 변경할 수 있으며, 메모리 주소를 다루기 때문에 큰 구조체를 복사하지 않고도 성능을 최적화할 수 있음
package main
import "fmt"
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func (c *Counter) GetCount() int {
return c.count
}
func main() {
counter := Counter{}
counter.Increment()
fmt.Println(counter.GetCount()) // 출력: 1
}
위 코드에서 Increment() 메서드는 포인터 리시버를 사용함
이 메서드는 구조체의 포인터를 전달받아, 원본 구조체의 count 값을 직접 수정할 수 있음
따라서 Increment() 메서드를 호출한 후 GetCount() 메서드를 통해 수정된 값을 확인할 수 있으며, 출력은 1이 됨
값 리시버와 포인터 리시버의 차이점
구분 | 값 리시버 | 포인터 리시버 |
구조체 전달 방식 | 구조체의 복사본 전달 | 구조체의 메모리 주소 전달 |
원본 수정 여부 | 원본 수정 불가능 | 원본 수정 가능 |
메모리 효율성 | 구조체가 클 경우 비효율적 (복사 비용) | 메모리 효율적 |
상태 변경 | 메서드 내에서 상태를 변경할 수 없음 | 메서드 내에서 상태를 변경할 수 있음 |
성능 차이
값 리시버는 구조체의 복사본을 사용하기 때문에, 큰 구조체에서는 메모리 복사로 인해 성능 저하가 발생할 수 있음
반면, 포인터 리시버는 구조체의 메모리 주소를 사용하므로 성능적으로 더 효율적
상태 변경
값 리시버는 구조체의 상태를 변경하지 않음
즉, 복사본에서만 변경이 일어나므로 원래의 구조체 상태에는 영향을 미치지 않음
반면, 포인터 리시버는 원본 구조체의 데이터를 직접 수정할 수 있어 상태 변경이 필요한 경우 적합함
언제 값 리시버를 사용하고, 언제 포인터 리시버를 사용해야 할까
- 값 리시버가 유리한 경우
- 구조체가 작고, 복사 비용이 적으며, 동시성 문제를 피해야 하는 경우 값 리시버가 유리함
- 값 리시버는 복사본을 사용하기 때문에 원본 데이터의 상태를 변경할 수 없으며, 여러 고루틴이 안전하게 동시 접근할 수 있음
- 포인터 리시버가 유리한 경우
- 구조체가 크거나, 원본 상태를 변경할 필요가 있는 경우 포인터 리시버가 유리함
- 그러나 동시성 문제를 신경 써야 하며, 성능 이점을 얻으려면 구조체 크기와 복사 비용을 고려해야 함
값 리시버와 포인터 리시버를 모두 사용하는 예제
하나의 구조체에 값 리시버와 포인터 리시버를 모두 사용할 수 있음
package main
import "fmt"
type Person struct {
name string
age int
}
// 값 리시버: 상태를 변경하지 않음
func (p Person) GetName() string {
return p.name
}
// 포인터 리시버: 상태를 변경함
func (p *Person) HaveBirthday() {
p.age++
}
func main() {
person := Person{name: "Alice", age: 25}
fmt.Println("이름:", person.GetName()) // 값 리시버 사용
person.HaveBirthday() // 포인터 리시버 사용
fmt.Println("나이:", person.age) // 출력: 26
}
GetName() 메서드는 값 리시버를 사용하여 데이터를 읽기만 하며, HaveBirthday() 메서드는 포인터 리시버를 사용하여 age 필드를 직접 수정함
'Go' 카테고리의 다른 글
Go 함수와 메서드 차이 (0) | 2024.06.24 |
---|---|
Go 구조체, 메서드, 인터페이스 (0) | 2024.06.19 |