Lidamos com interfaces no Go


Nos últimos meses, tenho conduzido um estudo perguntando às pessoas o que é difícil para elas entenderem no Go. E notei que o conceito de interfaces era mencionado regularmente nas respostas. Go foi a primeira linguagem de interface que usei, e lembro que naquele momento esse conceito parecia muito confuso. E neste guia, quero fazer o seguinte:

  1. Explicar na linguagem humana o que são interfaces.
  2. Explique como eles são úteis e como você pode usá-los no seu código.
  3. Fale sobre o que é a interface{} (uma interface vazia).
  4. E percorra alguns tipos de interface úteis que você pode encontrar na biblioteca padrão.

Então, o que é uma interface?


O tipo de interface no Go é um tipo de definição . Ele define e descreve os métodos específicos que algum outro tipo deve ter .

Um dos tipos de interface da biblioteca padrão é a interface fmt.Stringer :

 type Stringer interface { String() string } 

Dizemos que algo satisfaz essa interface (ou implementa essa interface ) se esse "algo" tiver um método com um valor específico de string de assinatura String() .

Por exemplo, o tipo Book satisfaz a interface porque possui o método String() :

 type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) } 

Não importa que tipo de Book ou o que faz. Tudo o que importa é que ele possui um método chamado String() que retorna um valor de string.

Aqui está outro exemplo. O tipo Count também satisfaz a interface fmt.Stringer , pois possui um método com o mesmo valor da string de assinatura String() .

 type Count int func (c Count) String() string { return strconv.Itoa(int(c)) } 

É importante entender aqui que temos dois tipos diferentes de Book e Count , que agem de maneira diferente. Mas eles estão unidos pelo fato de que ambos satisfazem a interface fmt.Stringer .

Você pode olhar do outro lado. Se você souber que o objeto atende à interface fmt.Stringer , pode assumir que ele possui um método com o valor da cadeia de assinatura String() que você pode chamar.

E agora a coisa mais importante.

Quando você vê uma declaração em Go (de uma variável, parâmetro de função ou campo de estrutura) que possui um tipo de interface, você pode usar um objeto de qualquer tipo, desde que satisfaça a interface.

Digamos que temos uma função:

 func WriteLog(s fmt.Stringer) { log.Println(s.String()) } 

Como WriteLog() usa o tipo de interface fmt.Stringer na fmt.Stringer parâmetro, podemos passar qualquer objeto que satisfaça a interface fmt.Stringer . Por exemplo, podemos passar os tipos Book e Count que criamos anteriormente no método WriteLog() , e o código funcionará bem.

Além disso, como o objeto passado satisfaz a interface fmt.Stringer , sabemos que ele possui um método String() , que pode ser chamado com segurança pela função WriteLog() .

Vamos juntar tudo em um exemplo, demonstrando o poder das interfaces.

 package main import ( "fmt" "strconv" "log" ) //   Book,    fmt.Stringer. type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) } //   Count,    fmt.Stringer. type Count int func (c Count) String() string { return strconv.Itoa(int(c)) } //   WriteLog(),    , //   fmt.Stringer   . func WriteLog(s fmt.Stringer) { log.Println(s.String()) } func main() { //   Book    WriteLog(). book := Book{"Alice in Wonderland", "Lewis Carrol"} WriteLog(book) //   Count    WriteLog(). count := Count(3) WriteLog(count) } 

Isso é demais. Na função principal, criamos diferentes tipos de Book e Count , mas os passamos para a mesma função WriteLog() . E ela chamou as funções apropriadas String() e escreveu os resultados no log.

Se você executar o código , obterá um resultado semelhante:

 2009/11/10 23:00:00 Book: Alice in Wonderland - Lewis Carrol 2009/11/10 23:00:00 3 

Não vamos nos deter sobre isso em detalhes. O principal a lembrar: usando o tipo de interface na declaração da função WriteLog() , tornamos a função indiferente (ou flexível) ao tipo do objeto recebido. O que importa é que métodos ele tem .

O que são interfaces úteis?


Existem várias razões pelas quais você pode começar a usar interfaces no Go. E, na minha experiência, os mais importantes são:

  1. As interfaces ajudam a reduzir a duplicação, ou seja, a quantidade de código padrão.
  2. Eles facilitam o uso de stubs em testes de unidade, em vez de objetos reais.
  3. Sendo uma ferramenta de arquitetura, as interfaces ajudam a desatar partes de sua base de código.

Vamos dar uma olhada mais de perto nessas maneiras de usar interfaces.

Reduza a quantidade de código padrão


Suponha que tenhamos uma estrutura de Customer contendo algum tipo de dados do cliente. Em uma parte do código, queremos gravar essas informações em bytes.Buffer , e na outra parte, queremos gravar dados do cliente no os.File no disco. Mas, nos dois casos, queremos primeiro serializar a estrutura ustomer para JSON.

Nesse cenário, podemos reduzir a quantidade de código padrão usando as interfaces Go.

Go tem um tipo de interface io.Writer :

 type Writer interface { Write(p []byte) (n int, err error) } 

E podemos aproveitar o fato de que bytes.Buffer e o tipo os.File satisfazem essa interface, porque eles possuem os métodos bytes.Buffer.Write () e os.File.Write () , respectivamente.

Implementação simples:

 package main import ( "encoding/json" "io" "log" "os" ) //   Customer. type Customer struct { Name string Age int } //   WriteJSON,   io.Writer   . //    ustomer  JSON,     // ,     Write()  io.Writer. func (c *Customer) WriteJSON(w io.Writer) error { js, err := json.Marshal(c) if err != nil { return err } _, err = w.Write(js) return err } func main() { //   Customer. c := &Customer{Name: "Alice", Age: 21} //    Buffer    WriteJSON var buf bytes.Buffer err := c.WriteJSON(buf) if err != nil { log.Fatal(err) } //   . f, err := os.Create("/tmp/customer") if err != nil { log.Fatal(err) } defer f.Close() err = c.WriteJSON(f) if err != nil { log.Fatal(err) } } 

Obviamente, este é apenas um exemplo fictício (podemos estruturar o código de maneira diferente para obter o mesmo resultado). Mas ilustra bem as vantagens do uso de interfaces: podemos criar o método Customer.WriteJSON() uma vez e chamá-lo toda vez que precisamos gravar em algo que satisfaça a interface io.Writer .

Mas se você é novo no Go, terá algumas perguntas: “ Como sei se a interface io.Writer existe? E como você sabe de antemão que ele está satisfeito com bytes.Buffer e os.File ? "

Receio que não haja uma solução simples. Você só precisa adquirir experiência, se familiarizar com as interfaces e os diferentes tipos da biblioteca padrão. Isso ajudará a ler a documentação desta biblioteca e a visualizar o código de outra pessoa. E para referência rápida, adicionei os tipos mais úteis de tipos de interface ao final do artigo.

Mas mesmo se você não usar interfaces da biblioteca padrão, nada o impede de criar e usar seus próprios tipos de interface . Falaremos sobre isso abaixo.

Teste de unidade e stubs


Para entender como as interfaces ajudam nos testes de unidade, vejamos um exemplo mais complexo.

Suponha que você tenha uma loja e informações sobre vendas e o número de clientes no PostgreSQL. Você deseja escrever um código que calcule o compartilhamento de vendas (número específico de vendas por cliente) do último dia, arredondado para duas casas decimais.

Uma implementação mínima seria assim:

 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr, err := calculateSalesRate(shopDB) if err != nil { log.Fatal(err) } fmt.Printf(sr) } func calculateSalesRate(sdb *ShopDB) (string, error) { since := time.Now().Sub(24 * time.Hour) sales, err := sdb.CountSales(since) if err != nil { return "", err } customers, err := sdb.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil } 

Agora, queremos criar um teste de unidade para a função calculateSalesRate() para verificar se os cálculos estão corretos.

Agora é problemático. Precisamos configurar uma instância de teste do PostgreSQL, além de criar e excluir scripts para preencher o banco de dados com dados falsos. Temos muito trabalho a fazer se realmente queremos testar nossos cálculos.

E as interfaces vêm em socorro!

CountSales() nosso próprio tipo de interface que descreve os CountSales() e CountCustomers() , nos quais a função CountCustomers() baseia. Em seguida, atualize a assinatura *ShopDB calculateSalesRate() para usar esse tipo de interface como parâmetro, em vez do tipo *ShopDB prescrito.

Assim:

 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) //    ShopModel.     //     ,     //  -,     . type ShopModel interface { CountCustomers(time.Time) (int, error) CountSales(time.Time) (int, error) } //  ShopDB    ShopModel,   //       -- CountCustomers()  CountSales(). type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr := calculateSalesRate(shopDB) fmt.Printf(sr) } //       ShopModel    //    *ShopDB. func calculateSalesRate(sm ShopModel) string { since := time.Now().Sub(24 * time.Hour) sales, err := sm.CountSales(since) if err != nil { return "", err } customers, err := sm.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil } 

Depois de fazer isso, apenas criaremos um esboço que satisfaça a interface do ShopModel . Em seguida, você pode usá-lo durante o teste de unidade da operação correta da lógica matemática na função calculateSalesRate() . Assim:

 // : main_test.go package main import ( "testing" ) type MockShopDB struct{} func (m *MockShopDB) CountCustomers() (int, error) { return 1000, nil } func (m *MockShopDB) CountSales() (int, error) { return 333, nil } func TestCalculateSalesRate(t *testing.T) { //  . m := &MockShopDB{} //     calculateSalesRate(). sr := calculateSalesRate(m) // ,        //   . exp := "0.33" if sr != exp { t.Fatalf("got %v; expected %v", sr, exp) } } 

Agora execute o teste e tudo funciona bem.

Arquitetura de aplicativos


No exemplo anterior, vimos como você pode usar interfaces para desacoplar determinadas partes do código de tipos específicos. Por exemplo, a função ShopModel calculateSalesRate() não importa o que você passa para ela, desde que satisfaça a interface ShopModel .

Você pode expandir essa ideia e criar níveis "desatados" inteiros em grandes projetos.
Suponha que você esteja criando um aplicativo Web que interaja com um banco de dados. Se você criar uma interface que descreva certos métodos para interagir com o banco de dados, poderá consultá-la em vez de um tipo específico através de manipuladores HTTP. Como os manipuladores HTTP se referem apenas à interface, isso ajudará a dissociar o nível HTTP e o nível de interação com o banco de dados. Será mais fácil trabalhar com níveis de forma independente e, no futuro, você poderá substituir alguns níveis sem afetar o trabalho de outros.

Eu escrevi sobre esse padrão em um dos posts anteriores , há mais detalhes e exemplos práticos.

O que é uma interface vazia?


Se você está programando o Go há algum tempo, provavelmente encontrou uma interface do tipo de interface vazia interface{} . Vou tentar explicar o que é. No começo deste artigo, escrevi:

O tipo de interface no Go é um tipo de definição . Ele define e descreve os métodos específicos que algum outro tipo deve ter .

Um tipo de interface vazio não descreve métodos . Ele não tem regras. E assim, qualquer objeto satisfaz uma interface vazia.

Em essência, o tipo de interface vazia interface{} é um tipo de curinga. Se você o encontrou em uma declaração (variável, parâmetro de função ou campo de estrutura), poderá usar um objeto de qualquer tipo .

Considere o código:

 package main import "fmt" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 fmt.Printf("%+v", person) } 

Aqui inicializamos o mapa para person , que usa um tipo de string para chaves e uma interface vazia do tipo de interface{} para valores. Atribuímos três tipos diferentes como valores de mapa (string, número inteiro e float32), e não há problema. Como objetos de qualquer tipo atendem à interface vazia, o código funciona muito bem.

Você pode executar este código aqui , verá um resultado semelhante:

 map[age:21 height:167.64 name:Alice] 

Quando se trata de extrair e usar valores de um mapa, é importante ter isso em mente. Suponha que você queira obter o valor da age e aumentá-lo em 1. Se você escrever um código semelhante, ele não será compilado:

 package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 person["age"] = person["age"] + 1 fmt.Printf("%+v", person) } 

Você receberá uma mensagem de erro:

 invalid operation: person["age"] + 1 (mismatched types interface {} and int) 

O motivo é que o valor armazenado no mapa pega o tipo de interface{} e perde seu tipo int base original. E como o valor não é mais inteiro, não podemos adicionar 1 a ele.

Para contornar isso, você precisa tornar o valor inteiro novamente e só então usá-lo:

 package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 age, ok := person["age"].(int) if !ok { log.Fatal("could not assert value to int") return } person["age"] = age + 1 log.Printf("%+v", person) } 

Se você executar isso , tudo funcionará conforme o esperado:

 2009/11/10 23:00:00 map[age:22 height:167.64 name:Alice] 

Então, quando você deve usar um tipo de interface vazia?

Talvez não com muita frequência . Se você chegar a esse ponto, pare e pense se é correto usar a interface{} . Como conselho geral, posso dizer que será mais claro, mais seguro e mais produtivo usar tipos específicos, ou seja, tipos de interface não vazios. No exemplo acima, era melhor definir uma estrutura de Person com campos adequadamente digitados:

 type Person struct { Name string Age int Height float32 } 

Uma interface vazia, por outro lado, é útil quando você precisa acessar e trabalhar com tipos imprevisíveis ou definidos pelo usuário. Por algum motivo, essas interfaces são usadas em locais diferentes da biblioteca padrão, por exemplo, nas funções gob.Encode , fmt.Print e template.Execute .

Tipos de interface úteis


Aqui está uma pequena lista dos tipos de interface mais solicitados e úteis da biblioteca padrão. Se você ainda não estiver familiarizado com eles, recomendo a leitura da documentação relevante.


Uma lista mais longa de bibliotecas padrão também está disponível aqui .

Source: https://habr.com/ru/post/pt463063/


All Articles