본문으로 바로가기

Go의 메서드에선 Receiver가 왜 필요한가?

 

여러 언어(java, kotlin, js ...)는 클래스 내부에 메서드를 명시합니다.

javascript의 경우 class 내부에 메서드를 명시할 수 있습니다. 아래와 같이요.

class Student {
	constructor() { ... }
    
    sayHello() {
      console.log("hello")
    }
}

 

그러나 Go에서는 구조체의 밖에 메서드가 있습니다. 따라서 특정 메서드가 어느 구조체의 것인지를 표시할 방법으로 Receiver를 사용합니다.

다음은  area 라는 이름의 메서드이고 s Square 부분이 Receiver(리시버)입니다.

그러니까 area 메서드는 Square 구조체의 메서드라는 것이죠.

func (s Square) area() int {
	return s.width * s.height
}

 

일반 함수와 비슷하게 생겨서 헷갈리는데, func와 함수 이름 사이에 리시버가 있으면 메서드, 아니면 일반 함수라고 생각하시면 됩니다.

근데 왜 굳이 메서드를 만들어야 할까요? 

package main

import "fmt"

type account struct {
	balance int
}

// 일반 함수
func withdrawFunc(a *account, amount int) {
	a.balance -= amount
}

// 메서드
func (a *account) withdrawMethod(amount int) {
	a.balance -= amount
}

func main() {
	a := &account{100}

	// 함수 형태 호출
	withdrawFunc(a, 30)
	fmt.Println(a.balance) // 70
	
	// 메서드 형태 호출
	a.withdrawMethod(30)
	fmt.Println(a.balance) // 40
}

 

 

method를 설정할 때 주의할 점

 

주의할 점은, struct의 주소가 아닌 일반 struct를 receiver로 넘기게 되면 메서드를 사용하는 struct가 아닌 struct 의 복사본에 내용을 적용합니다. 다시 말하지면 Go는 함수의 인자를 call by value로 받기 때문에 구조체를 그냥 넣으면 안됩니다.

func (a account) withdrawMethod(amount int) {
	// 아래의 경우 receiver는 해당 메서드를 이용하는 struct가 아닌 해당 struct의 복사본입니다.
	// 따라서 아래 조작은 해당 함수의 스코프를 벗어나면 아무 의미가 없음.
	a.balance -= amount
}

 

결론적으론 포인터를 이용하여 구조체를 조작해야 합니다. 이를 'pointer receiver'라고 부릅니다.

func (a *account) withdrawMethod(amount int) {
	a.balance -= amount
}

 

 

별칭 리시버 타입

 

별칭 타입도 리시버가 될 수 있습니다.

type HotDogInt int

func (a HotDogInt) add(b int) int {
	return int(a) + b
} 

func main() {
	var a HotDogInt = 10;
	output := a.add(5)
	fmt.Println(output) // 15
}

 

 

String 함수

 

조금 특수한 형태의 메서드를 알아보겠습니다. python에서 class를 그냥 출력하면 __str__ 값이 반환됩니다.

Go에도 비슷한 기능의 메서드가 존재합니다.

func (receiver *Account) String() string {
	return "this is String method!"
}
func main() {
    account := accounts.NewAccount("darren")
    fmt.Println(account) // &{darren 10}이 아니라 "this is String method!" 출력
}

 

 

Error 핸들링

 

아래와 같은 메서드를 작성했다고 가정합니다.

func (receiver *Account) Withraw(amount int) error {
	if receiver.balance < amount {
		return errors.New("Can't withraw")
	}
	receiver.balance -= amount
	// 반환이 error이므로 아무 반환도 하고 싶지 않더라도 nil 반환해야 함
	return nil
}

 

아래와 같이 err를 분리하여 필요할 때 넣어 편리하게 에러를 쓸 수 있습니다.

var errNoMoney = errors.New("no money. you can't withraw")

// Withraw method
func (receiver *Account) Withraw(amount int) error {
	if receiver.balance < amount {
		return errNoMoney
	}
	receiver.balance -= amount
	// 반환이 error이므로 아무 반환도 하고 싶지 않더라도 nil 반환해야 함
	return nil
}

 

 

그러나 error가 나야할지라도 에러를 일으키지 않고 조용히 실패합니다.

func main() {
	account := accounts.NewAccount("darren")
	account.Deposit(10)
	fmt.Println(account.Balance())
	account.Withraw(20) // silently fail
	fmt.Println(account.Balance())
}

 

만약 에러가 발생했을 때 block하고 싶다면 아래와 같이 해야 합니다.

func main() {
	account := accounts.NewAccount("darren")
	account.Deposit(10)
	fmt.Println(account.Balance())
	err := account.Withraw(20) 
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(account.Balance())
}

 

try/except 같은 구문은 없습니다. 위 방식에 익숙해집시다.

 

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체