17. 메서드
17. 메서드
✅ 1. 메서드 선언
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
type account struct {
balance int
}
func (a *account) withdraw(amount int) {
a.balance -= amount
}
func main() {
a := &account{100}
a.withdraw(20)
fmt.Printf("%d \n", a.balance)
}
(a *account)
와 같이func
키워드와 함수명 사이에 리시버를 정의해서 메서드를 선언한다- a와 같은 구조체 변수는 메서드 내에서 매개변수처럼 사용된다
- 리시버로는 해당 패키지 안에서
type
키워드로 선언된 로컬 타입만 가능하다 - 메서드는 구조체의 필드에 접근하는 것처럼 점 연산자로 호출한다
메서드 정의는 패키지 내의 어디에도 위치할 수 있다. 하지만 리시버 타입이 선언된 파일 내에서 정의하는 것이 일반적인 규칙이다.
1.1 별칭 리시버 타입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
type myInt int
func (mi myInt) add(b int) int {
return int(mi) + b
}
func main() {
var a myInt = 10
fmt.Println(a.add(5)) // 15
var b int = 10
fmt.Println(myInt(b).add(5)) // 15
}
- 모든 로컬 타입이 리시버 타입으로 가능하기 때문에 별칭 타입도 리시버가 될 수 있고 메서드를 가질 수 있다
int
와 같은 내장 타입도 별칭 타입을 활용하면 메서드를 가질 수 있다
✅ 2. 메서드는 왜 필요한가?
- 좋은 프로그래밍을 위해서는 결합도를 낮추고 응집도를 높여야 한다. 메서드는 데이터와 관련된 기능을 묶기 때문에 코드 응집도를 높이는 데 중요한 역할을 한다
- 현대 프로그래밍에서는 함수 호출 순서(절자치향)보다는 객체를 만들고 다른 객체와의 상호 작용을 맺는 것(객체지향)이 더 중요해졌고, 객체 간의 상호작용이 메서드로 표현된다
✅ 3. 포인터 메서드 vs 값 타입 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
import "fmt"
type account struct {
balance int
name string
}
func (a1 *account) withdraw_pointer(amount int) {
a1.balance -= amount
}
func (a2 account) withdraw_value(amount int) {
a2.balance -= amount // ineffective assignment to field account.balance
}
func (a3 account) withdraw_returnValue(amount int) account {
a3.balance -= amount
return a3
}
func main() {
var acc *account = &account{100, "Joe"}
acc.withdraw_pointer(30) // 1. 포인터 메서드 호출
fmt.Println(acc.balance) // 70
acc.withdraw_value(20) // 2. 값 타입 메서드 호출
fmt.Println(acc.balance) // 70
var acc2 account = acc.withdraw_returnValue(20) // 3. 값 타입 리턴 메서드 호출
fmt.Println(acc2.balance) // 50
}
- 포인터 메서드를 호출하면 포인터가 가리키는 메모리 주솟값이 복사되어 함수 내부, 외부가 동일한 인스턴스를 가리킨다
- 값 타입 메서드를 호출하면 인스턴스 복사가 일어나 함수 내부, 외부의 인스턴스는 서로 다른 메모리 주소를 가지게 된다
- 값 타입 메서드의 결과값을 반환하여 새로운 인스턴스에 대입하면 인스턴스 복사가 두 번 발생한다
- a3, acc2, acc 모두 다른 주소를 가지는 인스턴스
💡위 예제에서
*account
타입의acc
인스턴스로account
타입을 리시버로 가지는withdraw_value
메서드를 호출했다.
Go 언어에서는*Type
타입 인스턴스는Type
리시버 메서드와*Type
리시버 메서드를 모두 호출할 수 있다.
또한Type
타입 인스턴스는 원칙적으로Type
리시버 메서드만 가지고 있지만 해당 타입이 addressable하다면 컴파일러가&
를 붙여서*Type
타입 리시버 메서드도 호출할 수 있도록 해준다.
✅ 4. B.4 값 타입을 쓸 것인가? 포인터를 쓸 것인가?
4.1 B.4.1 성능에는 거의 차이가 없다
- 포인터가 물론 더 적은 메모리 복사가 이루어지긴 하지만 Go에서는 메모리를 많이 차지하는 슬라이스, 문자열, 맵 등이 모두 내부 포인털르 가지는 형태로 제작되어 있어 값 복사에 따른 메모리 낭비를 걱정하지 않아도 된다. (일반적인 경우엔)
4.2 B.4.2 객체 성격에 맞춰라
1
2
3
4
5
// time.Time 객체가 가지는 메서드 목록
type Time
func Now() Time
func (t Time) Add(d Duration) Time
func (t Time) After(u Time) bool
- Time 객체는 값 타입으로 사용되는 객체다
- 시간이라는 객체의 성격을 볼 때, 값(시간)이 변화하면 이전 시간과는 다른 객체가 되어야 하는 것이 타당하다
1
2
3
4
5
type Timer
func AfterFunc(d Duration, f func()) *Timer
func NewTimer(d Duration) *Timer
func (t *Timer) Reset(d Duration) bool
func (t *Timer) Stop() bool
- Timer 객체는 리시버 타입에 포인터를 사용한다
30초 이후에 알림을 주는 Timer 객체를 생성한 후 타이머가 종료되었다고 해서 이 Timer 객체가 다른 객체로 변하는 것은 타당하지 않다
- 두 방식에 문법적인 차이는 없지만 성격이 다르므로 개발자가 상황에 맞게 선택해야 한다
This post is licensed under CC BY 4.0 by the author.