At a Glance
type User struct {
Name string
Age int
}
func (u User) IsAdult() bool {
return u.Age >= 20
}
type Renamer interface {
Rename(string)
}
func (u *User) Rename(name string) {
u.Name = name
}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를 더 자주 검토
receiver 선택은 interface 만족에도 영향을 줍니다. T의 method set에는 value receiver만, *T의 method set에는 value와 pointer receiver가 모두 들어갑니다.
var _ Renamer = (*User)(nil) // 가능
// var _ Renamer = User{} // 불가: Rename은 pointer receiver변수 u := User{}에는 compiler가 필요할 때 &u로 pointer method를 호출할 수 있지만, interface에 User{} 값을 넣을 때는 자동 주소 변환을 기대할 수 없습니다. 상태를 바꾸는 타입은 receiver를 섞지 않고 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
}이렇게 두면 테스트 대역도 만들기 쉽고, 구현이 불필요하게 넓은 계약에 묶이지 않습니다.
interface 안의 typed nil은 nil이 아니다
interface 값은 동적 타입과 동적 값의 쌍입니다. 포인터가 nil이어도 동적 타입이 들어 있으면 interface 자체는 nil이 아닙니다.
var user *User = nil
var value any = user
pointerIsNil := user == nil // true
interfaceIsNil := value == nil // false
_, _ = pointerIsNil, interfaceIsNilfactory가 error나 interface를 반환할 때 nil 포인터 구현체를 그대로 돌려주면 호출자는 성공으로 보이는 non-nil interface를 받습니다. 오류가 없을 때는 명시적으로 return nil을 사용합니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 데이터를 묶는다 | struct |
| 상태를 바꾸지 않는 동작 | value receiver 검토 |
| 상태를 바꾸거나 복사를 줄이고 싶다 | pointer receiver 검토 |
| 필요한 계약만 추린다 | 작은 interface |
| 테스트 대역이 필요하다 | 소비자 쪽 interface |
주의할 점
Go interface를 너무 일찍, 너무 크게 정의하면 구현은 단순한데 계약만 커지는 경우가 많습니다. 또한 value receiver와 pointer receiver를 섞으면 어떤 타입이 어떤 interface를 만족하는지 헷갈리기 쉽습니다. 먼저 이 타입이 상태를 바꾸는가, 이 계약이 정말 여러 구현을 필요로 하는가를 같이 보세요.
참고 링크
3 sources