Skip to content

Go 메서드

Go 에서 메서드와 함수의 차이점은 메서드는 수신자를 가지지만 함수는 그렇지 않으며, 사용자 정의 타입만 메서드를 가질 수 있다는 점입니다. 먼저 예시를 보겠습니다.

go
type IntSlice []int

func (i IntSlice) Get(index int) int {
  return i[index]
}
func (i IntSlice) Set(index, val int) {
  i[index] = val
}

func (i IntSlice) Len() int {
  return len(i)
}

먼저 타입 IntSlice 를 선언하며, 하단 타입은 []int 입니다. 그리고 세 개의 메서드 Get, Set, Len 을 선언합니다. 메서드의 모양은 함수와 크게 다르지 않지만 (i IntSlice) 부분이 추가되었습니다. i 는 수신자이며, IntSlice 는 수신자의 타입입니다. 수신자는 다른 언어의 thisself 와 유사하지만, Go 에서는 명시적으로 지정해야 합니다.

go
func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())
}

메서드 사용은 클래스의 멤버 메서드를 호출하는 것과 유사합니다. 먼저 선언하고, 초기화한 후 호출합니다.

값 수신자

수신자는 두 가지 타입이 있습니다. 값 수신자와 포인터 수신자입니다. 먼저 예시를 보겠습니다.

go
type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // 수정되었지만 아무런 영향도 미치지 않습니다.
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

위 코드를 실행하면 myInt 의 값이 여전히 1 이며 2 로 수정되지 않았음을 알 수 있습니다. 메서드가 호출될 때 수신자의 값이 메서드로 전달됩니다. 위 예시의 수신자는 값 수신자로, 간단히 형식 매개변수로 볼 수 있으며, 형식 매개변수의 값을 수정해도 메서드 외부의 값에는 아무런 영향을 미치지 않습니다. 그렇다면 포인터로 호출하면 어떻게 될까요?

go
func main() {
  myInt := MyInt(1)
  (&myInt).Set(2)
  fmt.Println(myInt)
}

안타깝게도 이러한 코드는 여전히 내부 값을 수정할 수 없습니다. Go 는 수신자 타입과 일치시키기 위해 이를 역참조하여 (*(&myInt)).Set(2) 로 해석합니다.

포인터 수신자

약간 수정하면 myInt 의 값을 정상적으로 수정할 수 있습니다.

go
type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

이제 수신자는 포인터 수신자입니다. myInt 는 값 타입이지만, 값 타입을 통해 포인터 수신자의 메서드를 호출할 때 Go 는 이를 (&myint).Set(2) 로 해석합니다. 따라서 메서드의 수신자가 포인터일 때는 호출자가 포인터인지 여부와 관계없이 내부 값을 수정할 수 있습니다.

함수 매개변수 전달 과정에서 값 복사가 이루어집니다. 정수형이면 해당 정수를 복사하고, 슬라이스면 해당 슬라이스를 복사합니다. 하지만 포인터면 포인터만 복사하면 되므로, 포인터를 전달하는 것이 슬라이스를 전달하는 것보다 리소스 소모가 적습니다. 수신자도 마찬가지이며, 값 수신자와 포인터 수신자도 같은 원리입니다. 대부분의 경우 포인터 수신자를 사용하는 것이 권장되지만, 두 가지를 혼용해서는 안 됩니다. 모두 사용하거나 모두 사용하지 않아야 합니다. 아래 예시를 보겠습니다.

TIP

먼저 인터페이스 를 이해해야 합니다.

go
type Animal interface {
   Run()
}

type Dog struct {
}

func (d *Dog) Run() {
   fmt.Println("Run")
}

func main() {
   var an Animal
   an = Dog{}
   // an = &Dog{} 올바른 방식
   an.Run()
}

이 코드는 컴파일을 통과할 수 없으며, 컴파일러는 다음과 같은 오류를 출력합니다.

cannot use Dog{} (value of type Dog) as type Animal in assignment:
  Dog does not implement Animal (Run method has pointer receiver)

번역하면 Dog{}Animal 타입 변수를 초기화할 수 없으며, DogAnimal 을 구현하지 않았기 때문입니다. 해결 방법은 두 가지가 있습니다. 하나는 포인터 수신자를 값 수신자로 변경하는 것이고, 다른 하나는 Dog{}&Dog{} 로 변경하는 것입니다. 이제 각각 설명하겠습니다.

go
type Dog struct {
}

func (d Dog) Run() { // 값 수신자로 변경
   fmt.Println("Run")
}

func main() { // 정상 실행 가능
   var an Animal
   an = Dog{}
   // an = &Dog{} 역시 가능
   an.Run()
}

원래 코드에서 Run 메서드의 수신자는 *Dog 이므로, 자연스럽게 Animal 인터페이스를 구현하는 것은 Dog 포인터이며 Dog 구조체가 아닙니다. 이는 두 개의 다른 타입이므로, 컴파일러는 Dog{}Animal 의 구현이 아니라고 판단하여 변수 an 에 할당할 수 없습니다. 따라서 두 번째 해결 방법은 Dog 포인터를 변수 an 에 할당하는 것입니다. 값 수신자를 사용할 때는 Dog 포인터도 정상적으로 animal 에 할당할 수 있습니다. 이는 Go 가 적절한 상황에서 포인터를 역참조하기 때문입니다. 포인터를 통해 Dog 구조체를 찾을 수 있지만, 그 반대의 경우 Dog 구조체를 통해 Dog 포인터를 찾을 수 없습니다. 구조체에서 값 수신자와 포인터 수신자를 단순히 혼용하는 것은 문제가 없지만, 인터페이스와 함께 사용하면 오류가 발생합니다. 차라리 언제든 값 수신자만 사용하거나 포인터 수신자만 사용하여 좋은 규범을 형성하는 것이 좋으며, 이는 후속 유지보수 부담을 줄일 수 있습니다.

또 다른 상황은 값 수신자가 주소 지정 가능할 때 Go 가 자동으로 포인터 연산자를 삽입하여 호출한다는 점입니다. 예를 들어 슬라이스는 주소 지정 가능하므로 값 수신자를 통해 내부 값을 수정할 수 있습니다. 아래 코드를 보겠습니다.

go
type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func main() {
  s := make(Slice, 1)
  s.Set(0, 1)
  fmt.Println(s)
}

출력

[1]

하지만 요소를 추가하면 상황이 달라집니다. 아래 예시를 보겠습니다.

type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func (s Slice) Append(a int) {
  s = append(s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}
[1]

출력은 이전과 동일하며, append 함수는 반환값을 가지므로 슬라이스에 요소를 추가한 후 반드시 원본 슬라이스를 덮어써야 합니다. 특히 확장 후 값 수신자를 수정해도 아무런 영향을 미치지 않습니다. 이로 인해 예시의 결과가 발생하며, 포인터 수신자로 변경하면 정상입니다.

go
type Slice []int

func (s *Slice) Set(i int, v int) {
  (*s)[i] = v
}

func (s *Slice) Append(a int) {
  *s = append(*s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}

출력

[1 2]

Golang by www.golangdev.cn edit