Go 인터페이스 필드


105

Go에서 인터페이스는 데이터가 아닌 기능을 정의한다는 사실을 잘 알고 있습니다. 일련의 메소드를 인터페이스에 넣었지만 해당 인터페이스를 구현하는 모든 것에 필요한 필드를 지정할 수 없습니다.

예를 들면 :

// Interface
type Giver interface {
    Give() int64
}

// One implementation
type FiveGiver struct {}

func (fg *FiveGiver) Give() int64 {
    return 5
}

// Another implementation
type VarGiver struct {
    number int64
}

func (vg *VarGiver) Give() int64 {
    return vg.number
}

이제 인터페이스와 그 구현을 사용할 수 있습니다.

// A function that uses the interface
func GetSomething(aGiver Giver) {
    fmt.Println("The Giver gives: ", aGiver.Give())
}

// Bring it all together
func main() {
    fg := &FiveGiver{}
    vg := &VarGiver{3}
    GetSomething(fg)
    GetSomething(vg)
}

/*
Resulting output:
5
3
*/

이제 할 수없는 것은 다음과 같습니다.

type Person interface {
    Name string
    Age int64
}

type Bob struct implements Person { // Not Go syntax!
    ...
}

func PrintName(aPerson Person) {
    fmt.Println("Person's name is: ", aPerson.Name)
}

func main() {
    b := &Bob{"Bob", 23}
    PrintName(b)
}

그러나 인터페이스와 임베디드 구조체를 가지고 놀아 본 후, 저는이 작업을 수행하는 방법을 발견했습니다.

type PersonProvider interface {
    GetPerson() *Person
}

type Person struct {
    Name string
    Age  int64
}

func (p *Person) GetPerson() *Person {
    return p
}

type Bob struct {
    FavoriteNumber int64
    Person
}

포함 된 구조체 때문에 Bob은 Person이 가진 모든 것을 가지고 있습니다. 또한 PersonProvider 인터페이스를 구현하므로 해당 인터페이스를 사용하도록 설계된 함수에 Bob을 전달할 수 있습니다.

func DoBirthday(pp PersonProvider) {
    pers := pp.GetPerson()
    pers.Age += 1
}

func SayHi(pp PersonProvider) {
    fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}

func main() {
    b := &Bob{
        5,
        Person{"Bob", 23},
    }
    DoBirthday(b)
    SayHi(b)
    fmt.Printf("You're %v years old now!", b.Age)
}

다음은 위의 코드를 보여주는 Go Playground 입니다.

이 방법을 사용하면 동작이 아닌 데이터를 정의하는 인터페이스를 만들 수 있으며, 해당 데이터를 임베딩하는 것만으로 모든 구조체에서 구현할 수 있습니다. 포함 된 데이터와 명시 적으로 상호 작용하고 외부 구조체의 특성을 인식하지 못하는 함수를 정의 할 수 있습니다. 그리고 모든 것은 컴파일 타임에 확인됩니다! (내가 알 수있는 유일한 방법 은 콘크리트가 아닌에 인터페이스 PersonProvider를 포함하는 것입니다. 런타임에 컴파일되고 실패합니다.)BobPerson

자, 여기 내 질문이 있습니다. 이것은 깔끔한 속임수입니까, 아니면 다르게해야합니까?


4
"행동보다는 데이터를 정의하는 인터페이스를 만들 수 있습니다." 나는 당신이 데이터를 반환하는 행동을 가지고 있다고 주장합니다.
jmaloney 2014 년

나는 답을 쓸 것이다. 나는 당신이 그것을 필요로하고 그 결과를 안다면 괜찮다고 생각하지만, 결과가 있고 나는 그것을 항상하지 않을 것입니다.
twotwotwo 2014 년

@jmaloney 당신이 그것을 분명히보고 싶다면 당신이 옳다고 생각합니다. 그러나 전반적으로 내가 보여준 다른 부분으로 의미론은 "이 함수는 구성에 ___가있는 모든 구조체를 받아들입니다"가됩니다. 적어도 내가 의도 한 것입니다.
Matt Mc

1
이것은 "답변"자료가 아닙니다. "interface as struct property golang"을 검색하여 질문을 받았습니다. 인터페이스를 구현하는 구조체를 다른 구조체의 속성으로 설정하여 비슷한 접근 방식을 찾았습니다. 여기 놀이터입니다. play.golang.org/p/KLzREXk9xo 아이디어를 주셔서 감사합니다.
Dale

1
돌이켜 보면 Go를 사용한 지 5 년이 지난 후에는 위의 내용이 관용적 인 Go가 아님이 분명합니다. 제네릭에 대한 부담입니다. 이런 일을하고 싶은 마음이 든다면 시스템의 아키텍처를 재고 해보라고 조언합니다. 인터페이스를 수락하고 구조체를 반환하고 통신을 통해 공유하고 기뻐하십시오.
Matt Mc

답변:


55

확실히 깔끔한 트릭입니다. 그러나 포인터를 노출해도 여전히 사용 가능한 데이터에 직접 액세스 할 수 있으므로 향후 변경을위한 제한된 추가 유연성 만 구입합니다. 또한 Go 규칙은 데이터 속성 앞에 항상 추상화를 두지 않아도 됩니다.

이러한 것들을 종합하면, 나는 주어진 사용 사례에 대해 한쪽 또는 다른 쪽을 향하는 경향이 있습니다. a) 공개 속성 (해당되는 경우 임베딩 사용)을 만들고 구체적인 유형을 전달하거나 b) 데이터를 노출하는 것처럼 보이는 경우 나중에 문제가 발생하면 더 강력한 추상화를 위해 getter / setter를 노출하십시오.

당신은 속성별로 이것을 평가할 것입니다. 예를 들어, 일부 데이터가 구현에 따라 다르거 나 다른 이유로 표현을 변경하려는 경우 속성을 직접 노출하고 싶지 않은 반면 다른 데이터 속성은 공개로 만드는 것이 순익이 될만큼 안정적 일 수 있습니다.


getter 및 setter 뒤에 속성을 숨기면 나중에 이전 버전과 호환되는 변경을 수행 할 수있는 추가 유연성이 제공됩니다. 언젠가 Person는 단일 "이름"필드가 아니라 이름 / 중간 / 성 / 접두사를 저장 하도록 변경하고 싶다고 가정 해 보겠습니다 . Name() string및 메서드가있는 경우 새로운 세분화 된 메서드를 추가하는 동안 인터페이스 SetName(string)의 기존 사용자를 Person만족 시킬 수 있습니다 . 또는 저장되지 않은 변경 사항이있는 경우 데이터베이스 지원 개체를 "더티"로 표시 할 수 있습니다. 데이터 업데이트가 모두 SetFoo()메소드를 거치면 그렇게 할 수 있습니다 .

따라서 getter / setter를 사용하면 호환되는 API를 유지하면서 구조체 필드를 변경할 수 있으며, 아무도 p.Name = "bob"코드를 거치지 않고 는 할 수 없기 때문에 속성 get / set에 대한 논리를 추가 할 수 있습니다 .

이러한 유연성은 유형이 복잡하고 코드베이스가 클 때 더 적합합니다. 가있는 경우 , a , a 데이터베이스 ID 또는 기타 항목에 PersonCollection의해 내부적으로 지원 될 수 있습니다 . 올바른 인터페이스를 사용 하면 네트워크 연결과 파일을 똑같이 보이게 하는 방식으로 발신자가 신경 쓰지 않도록 할 수 있습니다 .sql.Rows[]*Person[]uintio.Reader

한 가지 구체적인 사항 : interfaceGo의 s에는 정의하는 패키지를 가져 오지 않고도 구현할 수있는 고유 한 속성이 있습니다. 순환 가져 오기방지 할 수 있습니다 . 인터페이스가 *Person문자열이나 다른 것 대신를 반환하면 모두 정의 된 PersonProviders패키지를 가져와야합니다 Person. 그것은 괜찮을 수도 있고 심지어 불가피 할 수도 있습니다. 알아야 할 결과 일뿐입니다.


그러나 Go 커뮤니티에는 유형의 공용 API에서 데이터 멤버를 노출하는 것에 대한 강력한 규칙이 없습니다 . 오히려 낙담보다는, 주어진 경우에 대비하여 API의 일부로 속성에 대한 공용 액세스를 사용하는 것이 합리적인지의 판단에 남아 있는 노출이 가능 복잡한 수 이상을 구현 변화를 방지하기 때문이다.

예를 들어 stdlib는 http.Server구성으로 를 초기화 하고 0 bytes.Buffer을 사용할 수 있다고 약속하는 것과 같은 작업을 수행합니다 . 그런 식으로 자신의 작업을 수행하는 것은 괜찮습니다. 실제로 더 구체적이고 데이터를 노출하는 버전이 작동 할 것 같다면 선제 적으로 추상화해서는 안된다고 생각합니다. 장단점을 인식하는 것뿐입니다.


한 가지 추가 : 임베딩 접근 방식은 상속과 비슷합니다. 포함 된 구조체에있는 모든 필드와 메서드를 가져오고 인터페이스를 사용하여 인터페이스 집합을 다시 구현하지 않고도 모든 상위 구조체가 한정되도록 할 수 있습니다.
Matt Mc

예, 다른 언어의 가상 상속과 매우 유사합니다. 임베딩을 사용하여 getter 및 setter 또는 데이터에 대한 포인터 (또는 작은 구조체에 대한 읽기 전용 액세스를위한 세 번째 옵션, 구조체의 복사본)로 정의 된 인터페이스를 구현할 수 있습니다.
twotwotwo 2014 년

나는 이것이 1999 년으로의 회상을주고 Java로 상용구 게터와 세터를 작성하는 방법을 배우고 있다고 말해야한다.
Tom

너무 나쁘다 Go의 자체 표준 라이브러리가 항상 이렇게하는 것은 아닙니다. 단위 테스트를 위해 os.Process에 대한 일부 호출을 조롱하는 중입니다. Pid 멤버 변수가 직접 액세스되고 Go 인터페이스가 멤버 변수를 지원하지 않기 때문에 인터페이스에서 프로세스 개체를 래핑 할 수 없습니다.
Alex Jansen

1
@Tom 사실입니다. 나는 게터 / 세터 포인터를 노출보다 더 유연 생각하지만, 나는 또한 모두해야 게터 / 세터-쓸어 모든 (또는 일반적인 이동에 맞 것이라고) 생각하지 않습니다. 나는 이전에 그것을 몸짓으로하는 몇 마디를 가지고 있었지만 그것을 훨씬 더 강조하기 위해 시작과 끝을 수정했다.
twotwotwo

2

내가 올바르게 이해한다면 하나의 구조체 필드를 다른 구조체 필드에 채우고 싶습니다. 확장을 위해 인터페이스를 사용하지 않는 것이 제 의견입니다. 다음 접근 방식으로 쉽게 할 수 있습니다.

package main

import (
    "fmt"
)

type Person struct {
    Name        string
    Age         int
    Citizenship string
}

type Bob struct {
    SSN string
    Person
}

func main() {
    bob := &Bob{}

    bob.Name = "Bob"
    bob.Age = 15
    bob.Citizenship = "US"

    bob.SSN = "BobSecret"

    fmt.Printf("%+v", bob)
}

https://play.golang.org/p/aBJ5fq3uXtt

참고 PersonBob선언. 이렇게하면 포함 된 struct 필드 Bob를 일부 구문 설탕과 함께 구조에서 직접 사용할 수 있습니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.