Campos de interface Go

105

Estou familiarizado com o fato de que, em Go, as interfaces definem funcionalidade, em vez de dados. Você coloca um conjunto de métodos em uma interface, mas não consegue especificar nenhum campo que seria necessário em qualquer coisa que implemente essa interface.

Por exemplo:

// 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
}

Agora podemos usar a interface e suas implementações:

// 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
*/

Agora, o que você não pode fazer é algo assim:

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)
}

No entanto, depois de brincar com interfaces e estruturas incorporadas, descobri uma maneira de fazer isso, de certa forma:

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
}

Por causa da estrutura incorporada, Bob tem tudo que Person tem. Ele também implementa a interface PersonProvider, para que possamos passar Bob para funções projetadas para usar essa interface.

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)
}

Aqui está um Go Playground que demonstra o código acima.

Usando esse método, posso fazer uma interface que define dados em vez de comportamento, e que pode ser implementada por qualquer estrutura apenas incorporando esses dados. Você pode definir funções que interagem explicitamente com esses dados incorporados e não estão cientes da natureza da estrutura externa. E tudo é verificado em tempo de compilação! (A única maneira de você bagunçar, pelo que eu posso ver, seria incorporar a interface PersonProviderem Bob, em vez de um concreto Person. Ele iria compilar e falhar no tempo de execução.)

Agora, aqui está a minha pergunta: este é um truque legal ou eu deveria fazer diferente?

Matt Mc
fonte
3
“Posso fazer uma interface que defina dados ao invés de comportamento”. Eu diria que você tem um comportamento que retorna dados.
jmaloney
Vou escrever uma resposta; Acho que está tudo bem se você precisa e sabe as consequências, mas existem consequências e eu não faria isso o tempo todo.
twotwotwo
@jmaloney Acho que você está certo, se quiser ver isso com clareza. Mas no geral, com as diferentes peças que mostrei, a semântica se torna "esta função aceita qualquer estrutura que tenha um ___ em sua composição". Pelo menos, era isso que eu pretendia.
Matt Mc
1
Este não é um material de "resposta". Eu respondi sua pergunta pesquisando "interface as struct golang" no Google. Encontrei uma abordagem semelhante definindo uma estrutura que implementa uma interface como propriedade de outra estrutura. Aqui está o playground, play.golang.org/p/KLzREXk9xo Obrigado por me dar algumas idéias.
Dale
1
Em retrospecto, e após 5 anos de uso do Go, está claro para mim que o Go acima não é idiomático. É um esforço para os genéricos. Se você se sentir tentado a fazer esse tipo de coisa, aconselho-o a repensar a arquitetura do seu sistema. Aceite interfaces e retorne structs, compartilhe comunicando-se e alegre-se.
Matt Mc

Respostas:

55

É definitivamente um truque legal. No entanto, a exposição de ponteiros ainda disponibiliza acesso direto aos dados, de modo que só oferece flexibilidade adicional limitada para alterações futuras. Além disso, as convenções Go não exigem que você sempre coloque uma abstração na frente dos atributos de dados .

Juntando essas coisas, eu tenderia para um extremo ou outro para um determinado caso de uso: ou a) apenas crie um atributo público (usando incorporação, se aplicável) e passe tipos concretos ou b) se parecer que expor os dados, causar problemas mais tarde, exponha um getter / setter para uma abstração mais robusta.

Você vai pesar isso por atributo. Por exemplo, se alguns dados são específicos da implementação ou você espera alterar as representações por algum outro motivo, provavelmente não deseja expor o atributo diretamente, enquanto outros atributos de dados podem ser estáveis ​​o suficiente para torná-los públicos uma vitória líquida.


Ocultar propriedades atrás de getters e setters oferece flexibilidade extra para fazer alterações compatíveis com versões anteriores posteriormente. Digamos que algum dia você queira alterar Personpara armazenar não apenas um único campo de "nome", mas o primeiro / meio / último / prefixo; se você tem métodos Name() stringe SetName(string), pode manter os usuários existentes da Personinterface felizes enquanto adiciona novos métodos mais refinados. Ou você pode querer marcar um objeto de banco de dados como "sujo" quando ele tem alterações não salvas; você pode fazer isso quando as atualizações de dados passarem por SetFoo()métodos.

Assim: com getters / setters, você pode alterar os campos de estrutura enquanto mantém uma API compatível e adicionar lógica em torno de get / sets de propriedade, já que ninguém pode simplesmente p.Name = "bob"passar sem passar pelo seu código.

Essa flexibilidade é mais relevante quando o tipo é complicado (e a base de código é grande). Se você tiver um PersonCollection, ele pode ser apoiado internamente por um sql.Rows, um []*Person, um []uintde IDs de banco de dados ou qualquer outro. Usando a interface certa, você pode evitar que os chamadores se importem com isso, da maneira como io.Readeras conexões de rede e os arquivos parecem semelhantes.

Uma coisa específica: interfaces no Go têm a propriedade peculiar de que você pode implementar um sem importar o pacote que o define; que pode ajudá-lo a evitar importações cíclicas . Se sua interface retornar um *Person, ao invés de apenas strings ou o que for, todos PersonProvidersterão que importar o pacote onde Personestá definido. Isso pode ser bom ou mesmo inevitável; é apenas uma consequência a ser conhecida.


Mas, novamente, a comunidade Go não tem uma convenção forte contra a exposição de membros de dados na API pública do seu tipo . É deixado a seu critério se é razoável usar o acesso público a um atributo como parte de sua API em um determinado caso, em vez de desencorajar qualquer exposição porque isso poderia complicar ou impedir uma mudança de implementação posteriormente.

Assim, por exemplo, o stdlib faz coisas como permitir que você inicialize um http.Servercom sua configuração e promete que um zero bytes.Bufferpode ser usado. É bom fazer suas próprias coisas desse jeito e, de fato, não acho que você deva abstrair as coisas preventivamente se a versão mais concreta, expondo os dados, parece funcionar. É apenas uma questão de estar ciente das vantagens e desvantagens.

dois e dois
fonte
Uma coisa adicional: a abordagem de incorporação é um pouco mais parecida com herança, certo? Você obtém todos os campos e métodos que a estrutura incorporada possui e pode usar sua interface para que qualquer superestrutura seja qualificada, sem reimplementar conjuntos de interfaces.
Matt Mc
Sim - muito parecido com a herança virtual em outras línguas. Você pode usar a incorporação para implementar uma interface, seja ela definida em termos de getters e setters ou um ponteiro para os dados (ou, uma terceira opção para acesso somente leitura a pequenas estruturas, uma cópia da estrutura).
twotwotwo
Devo dizer que isso está me dando flashbacks de 1999 e aprendendo a escrever resmas de getters e setters clichês em Java.
Tom
É uma pena que a biblioteca padrão do próprio Go nem sempre faça isso. Estou tentando simular algumas chamadas para os.Process para testes de unidade. Não posso simplesmente envolver o objeto de processo em uma interface, pois a variável de membro Pid é acessada diretamente e as interfaces Go não oferecem suporte a variáveis ​​de membro.
Alex Jansen
1
@Tom Isso é verdade. Eu acho que getters / setters adicionam mais flexibilidade do que expor um ponteiro, mas também não acho que todos devam getter / setter-ify tudo (ou que corresponderia ao estilo típico de Go). Anteriormente, eu tinha algumas palavras apontando para isso, mas revisei o início e o fim para enfatizar muito mais.
twotwotwo
2

Se bem entendi, você deseja preencher os campos de um struct em outro. Minha opinião é não usar interfaces para estender. Você pode fazer isso facilmente pela próxima abordagem.

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

Nota Personna Bobdeclaração. Isso fará com que o campo de estrutura incluído esteja disponível na Bobestrutura diretamente com algum açúcar sintático.

Igor A. Melekhine
fonte