golang 기초 - 메서드

한나리·2025년 1월 9일

Go

목록 보기
10/19
post-thumbnail

메서드

메서드는 함수의 일종
Go언어에 클래스가 없기때문에 구조체 밖에서 메서드를 지정
구조체 밖에 메소드를 정의할 때 리시버라는 기능 사용
함수와는 다르게 리시버를 넣고, 리시버를 통해 호출할 수 있음.

리시버

구조체 밖에 메서드가 있어서 메서드가 어느 구조체에 속하는지 표시할 방법이 필요한데, 이때 리시버 사용
리시버는 메서드가 속하는 타입을 알려주는 기법

메서드 정의

func (리시버) 메서드명 (매개변수명 매개변수타입) 반환타입 {
	코드
    return 반환값   
}

func (r Rabbit) info() int {
	return r.width * r.height
}

리시버가 있으므로 info() 메서드가 Rabbit 타입에 속한다는 것을 알 수 있음
이때 구조체 변수 r은 해당 메서드에서 매개변수처럼 사용

🔎 메서드 정의는 같은 패키지 내 어디에도 위치할 수 있음. 하지만 리시버 타입이 선언된 파일 안에 정의하는 게 일반적인 규칙임.
예를 들어, type Student struct 구조체를 student.go 파일에 정의했으면 Student의 메서드들도 모두 student.go 파일에 모아놓음

별칭 리시버 타입

모든 로컬 타입은 리시버 타입으로 선언 가능. 다만 기본 타입은 별칭 타입을 만들고 그 타입의 메서드를 선언해서 사용해야 함.
따라서 별칭 타입도 리시버가 될 수 있고, 메서드를 가질 수 있음.

로컬 타입
로컬 타입: 기본 타입(int, string, 등)과 이를 기반으로 정의된 별칭 타입, 구조체, 배열, 슬라이스 등이 포함

✓ 기본 타입 자체는 리시버가 될 수 없다
Go에서 메서드를 정의하려면, 리시버 타입은 반드시 다음 조건을 만족해야 한다.
1️⃣ 로컬 타입
즉, 해당 메서드가 정의된 패키지 안에서 선언된 타입이어야 함.
2️⃣ Go의 내장 기본 타입은 로컬 타입으로 간주되지 않으므로 직접적으로 메서드를 정의할 수 없음.
→ Go에서 기본 타입을 기반으로 사용자 정의 타입(별칭 타입)을 선언하면, 해당 타입을 리시버로 사용할 수 있습니다.

 package main

import "fmt"

// 기본 타입 int를 기반으로 사용자 정의 타입 생성
type MyInt int

// 사용자 정의 타입에 메서드 추가
func (m MyInt) Double() int {
    return int(m * 2)
}

func main() {
    var num MyInt = 10
    fmt.Println(num.Double()) // 출력: 20
}

함수 vs 메서드

1️⃣ 소속이 다름

일반 함수는 어디에도 속하지 않지만, 메서드는 리시버에 속한다

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{balance: 100} 
	withdrawFunc(a, 30)          // 함수 형태 호출
	fmt.Println(a.balance)       //70

	a.withdrawMethod(30)   // 메서드 형태 호출
	fmt.Println(a.balance) //40

}

withdrawFunc는 account 타입과 분리되어 있지만 withdrawMethod는 account 타입에 속한 메서드이며, account의 기능이다. 메서드를 사용해서 데이터와 기능을 묶을 수 있게 된다.

2️⃣ 객체지향 : 절차 중심에서 관계 중심으로

데이터와 기능이 분리되어 어떤 순서로 실행하는지가 중요한 절차 중심에서
메서드를 통해 데이터와 기능을 묶을 수 있게 되고, 데이터와 기능이 묶인 단일 객체 로써 동작하게 되면서 객체 간 관계 중심으로 변화

이러한 객체 인스턴스들이 서로 유기적으로 소통하고 관계 맺게 됨에 따라 절차보다 객체 간 관계 중심으로 프로그래밍 하는것을 객체지향 프로그래밍이라고 부름

포인터 메서드 vs 값 타입 메서드

package main

import "fmt"

type accounts struct {
	balance   int
	firstName string
	lastName  string
}

// 값 타입 메서드
func (a accounts) withdrawValue(amount int) { //일반 함수 표현
	a.balance -= amount
	fmt.Println("withdrawValue : a의 balance : ", a.balance)
}

// 포인터 메서드
func (a *accounts) withdrawPointer(amount int) { //메서드 표현
	a.balance -= amount
	fmt.Println("withdrawPointer : a의 balance : ", a.balance)

}

// 변경된 값을 반환하는 값 타입 메서드
func (a accounts) withdrawReturnValue(amount int) accounts {
	a.balance -= amount
	fmt.Println("withdrawReturnValue : a의 balance : ", a.balance)

	return a
}

func main() {
	var mainA *accounts = &accounts{100, "Joe", "Park"}
	mainA.withdrawPointer(30)  // 메모리 주솟값만 복사, a와 mainA(100)는 같은 인스턴스를 가리킴 : 주소값 참조
	fmt.Println(mainA.balance) //70 포인터 메서드기때문에 mainA.balance도 같이 변함

	mainA.withdrawValue(20)    //mainA(70)의 모든 값이 a에 복사 : 값 복사
	fmt.Println(mainA.balance) //a는 50이었겠지만, mainA의 값은 변경되지 않아서 70 그대로

	var mainB accounts = mainA.withdrawReturnValue(20) //mainA의 모든 값(70)이 복사된 채로 로직을 타고 20 인출 결과를 mainB(50)에 할당 : 값 복사
	fmt.Println(mainB.balance)                         //mainB는 값타입 이라서 50

	mainB.withdrawPointer(30)  //mainB의 주소값만 복사, a와 mainB는 같은 인스턴스를 가리킴 : 주소값 참조
	fmt.Println(mainB.balance) //mainB자체가 변해서 20
}

포인터 메서드는 메서드 내부에서 리시버의 값을 변경시킬 수 있음.
값 타입 메서드는 호출하는 쪽과 메서드 내부의 값은 별도 인스턴스로 독립되기 때문에 메서드 내부에서 리시버의 값을 변경시킬 수 없음. 호출자 인스턴스에 접근할 수 없고 복사되는 양에 따라서 성능상 문제가 될 수 있음

포인터 메서드 : 인스턴스 중심
값 타입 메서드 : 값 중심

profile
내가 떠나기 전까지는 망하지 마라, 블록체인 개발자

0개의 댓글