핵심 정리
type User struct {
Name string
Age int
}
func (u User) IsAdult() bool {
return u.Age >= 20
}Go는 클래스보다 데이터는 struct, 동작은 method, 계약은 interface로 나눠 읽는 편이 맞습니다.
구조 이해
struct는 데이터를 묶는 기본 단위다
type User struct {
Name string
Age int
}Go의 struct는 필드를 묶는 기본 구성 단위입니다. 상속 계층보다 조합과 명시적 필드 구성이 더 앞에 옵니다.
method receiver가 동작의 소속을 정한다
func (u User) IsAdult() bool {
return u.Age >= 20
}receiver가 값이면 복사본 기준 동작이고, 포인터면 원본을 바꾸거나 큰 복사를 피하려는 의도가 들어갑니다.
func (u *User) Rename(name string) {
u.Name = name
}- 읽기 중심, 작은 값 타입: value receiver 가능
- 상태 변경, 큰 struct, 일관된 메서드 집합: pointer receiver를 더 자주 검토
Go interface는 암시적으로 만족된다
Go에서는 implements를 쓰지 않습니다. 메서드 집합이 맞으면 자동으로 그 interface를 만족합니다.
type Reader interface {
Read(p []byte) (n int, err error)
}어떤 타입이든 Read 메서드를 가지면 Reader처럼 쓸 수 있습니다.
func consume(r io.Reader) error {
// ...
return nil
}interface는 보통 "필요한 쪽"에 둔다
Go에서는 interface를 구현 타입 옆에 크게 미리 선언하기보다, 그 동작이 필요한 소비자 쪽에서 작은 계약으로 두는 편이 흔합니다.
type Clock interface {
Now() time.Time
}이렇게 두면 테스트 대역도 만들기 쉽고, 구현이 불필요하게 넓은 계약에 묶이지 않습니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 데이터를 묶는다 | struct |
| 상태를 바꾸지 않는 동작 | value receiver 검토 |
| 상태를 바꾸거나 복사를 줄이고 싶다 | pointer receiver 검토 |
| 필요한 계약만 추린다 | 작은 interface |
| 테스트 대역이 필요하다 | 소비자 쪽 interface |
주의할 점
Go interface를 너무 일찍, 너무 크게 정의하면 구현은 단순한데 계약만 커지는 경우가 많습니다. 또한 value receiver와 pointer receiver를 섞으면 어떤 타입이 어떤 interface를 만족하는지 헷갈리기 쉽습니다. 먼저 이 타입이 상태를 바꾸는가, 이 계약이 정말 여러 구현을 필요로 하는가를 같이 보세요.
참고 링크
3 sources