Usar interfaces em Go

Concluído

Interfaces em Go são um tipo de dados que é usado para representar o comportamento de outros tipos. Uma interface é como um plano ou um contrato que um objeto deve satisfazer. Quando você usa interfaces, sua base de código se torna mais flexível e adaptável, porque você está escrevendo código que não está vinculado a uma implementação específica. Portanto, você pode estender a funcionalidade de um programa rapidamente. Você entende o porquê neste módulo.

Ao contrário das interfaces em outras linguagens de programação, as interfaces em Go são satisfeitas implicitamente. O Go não oferece palavras-chave para implementar uma interface. Então, se você está familiarizado com interfaces em outras linguagens de programação, mas é novo no Go, essa ideia pode ser confusa.

Neste módulo, trabalhamos com vários exemplos para explorar interfaces no Go e demonstrar como aproveitá-las ao máximo.

Declarar uma interface

Uma interface no Go é como um projeto. Um tipo abstrato que inclui apenas os métodos que um tipo concreto deve possuir ou implementar.

Digamos que você queira criar uma interface em seu pacote de geometria que indique quais métodos uma forma deve implementar. Você pode definir uma interface como esta:

type Shape interface {
    Perimeter() float64
    Area() float64
}

A Shape interface significa que qualquer tipo que você queira considerar precisa Shape ter os Perimeter() métodos e Area() . Por exemplo, quando você cria uma Square estrutura, ela tem que implementar os dois métodos, não apenas um. Além disso, observe que uma interface não contém detalhes de implementação para esses métodos (por exemplo, para calcular o perímetro e a área de uma forma). São simplesmente um contrato. Formas como triângulos, círculos e quadrados têm diferentes maneiras de calcular a área e o perímetro.

Implementar uma interface

Como discutimos anteriormente, em Go, você não tem uma palavra-chave para implementar uma interface. Uma interface em Go é satisfeita implicitamente por um tipo quando tem todos os métodos que uma interface exige.

Vamos criar um Square struct que tenha ambos os métodos da Shape interface, conforme mostrado no código de exemplo a seguir:

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}

Observe como a assinatura do método do Square struct corresponde à assinatura da Shape interface. No entanto, outra interface pode ter um nome diferente, mas os mesmos métodos. Como ou quando a Go sabe qual interface um tipo de concreto está implementando? Go sabe quando você está usando, em tempo de execução.

Para demonstrar como as interfaces são usadas, você pode escrever o seguinte código:

func main() {
    var s Shape = Square{3}
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

Quando você executa o programa anterior, você obtém a seguinte saída:

main.Square
Area:  9
Perimeter: 12

Neste ponto, não faz diferença se você usa ou não uma interface. Vamos criar outro tipo, como Circle, e explorar por que as interfaces são úteis. Aqui está o código para o Circle struct:

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

Agora vamos refatorar a main() função e criar uma função que possa imprimir o tipo do objeto que recebe, juntamente com sua área e perímetro, assim:

func printInformation(s Shape) {
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
    fmt.Println()
}

Observe como a printInformation função tem Shape como parâmetro. Você pode enviar um Square ou um Circle objeto para essa função, e ela funciona, embora a saída seja diferente. Sua main() função agora tem esta aparência:

func main() {
    var s Shape = Square{3}
    printInformation(s)

    c := Circle{6}
    printInformation(c)
}

Observe que, para o c objeto, não especificamos que é um Shape objeto. No entanto, a printInformation função espera um objeto que implementa os métodos que são definidos na Shape interface.

Quando você executa o programa, você deve obter a seguinte saída:

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

Observe como você não recebe um erro, e a saída varia dependendo do tipo de objeto recebido. Você também pode ver que o tipo de objeto na saída não diz nada sobre a Shape interface.

A beleza de usar interfaces é que, para cada novo tipo ou implementação do Shape, a printInformation função não precisa mudar. Como dissemos anteriormente, seu código se torna mais flexível e mais fácil de estender quando você usa interfaces.

Implementar uma interface Stringer

Um exemplo simples de extensão da funcionalidade existente é usar um Stringer, que é uma interface que tem um String() método, como este:

type Stringer interface {
    String() string
}

A fmt.Printf função usa essa interface para imprimir valores, o que significa que você pode escrever seu método personalizado String() para imprimir uma cadeia de caracteres personalizada, assim:

package main

import "fmt"

type Person struct {
    Name, Country string
}

func (p Person) String() string {
    return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
    rs := Person{"John Doe", "USA"}
    ab := Person{"Mark Collins", "United Kingdom"}
    fmt.Printf("%s\n%s\n", rs, ab)
}

Quando você executa o programa anterior, você obtém a seguinte saída:

John Doe is from USA
Mark Collins is from United Kingdom

Como você pode ver, você usou um tipo personalizado (um struct) para escrever uma versão personalizada do String() método. Esta técnica é uma maneira comum de implementar uma interface em Go, e você encontra exemplos dela em muitos programas, como estamos prestes a explorar.

Estender uma implementação existente

Digamos que você tenha o código a seguir e gostaria de estender sua funcionalidade escrevendo uma implementação personalizada de um Writer método encarregado de manipular alguns dados.

Usando o código a seguir, você pode criar um programa que consome a API do GitHub para obter três repositórios da Microsoft:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=3")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    io.Copy(os.Stdout, resp.Body)
}

Quando você executa o código anterior, você obtém algo como a seguinte saída (abreviada para legibilidade):

[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

Observe que a io.Copy(os.Stdout, resp.Body) chamada é aquela que imprime no terminal o conteúdo obtido da chamada para a API do GitHub. Digamos que você queira escrever sua própria implementação para encurtar o conteúdo que você vê no terminal. Quando você olha para a fonte da io.Copy função, você vê:

func Copy(dst Writer, src Reader) (written int64, err error)

Se você se aprofundar nos detalhes do primeiro parâmetro, dst Writernotará que Writer é uma interface:

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

Você pode continuar explorando o código-fonte do io pacote até encontrar onde Copy chama o Write método, mas vamos deixar essa exploração em paz por enquanto.

Como Writer é uma interface e é um objeto que a Copy função está esperando, você pode escrever sua implementação personalizada do Write método. Portanto, você pode personalizar o conteúdo que você imprime no terminal.

A primeira coisa que você precisa para implementar uma interface é criar um tipo personalizado. Nesse caso, você pode criar uma estrutura vazia, porque você simplesmente precisa escrever seu método personalizado Write , assim:

type customWriter struct{}

Agora você está pronto para escrever sua função personalizada Write . Você também precisa escrever um struct para analisar a resposta da API no formato JSON para um objeto Golang. Você pode usar o site JSON-to-Go para criar uma struct a partir de uma carga JSON útil. Portanto, o Write método pode ter esta aparência:

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

Finalmente, você tem que modificar a main() função para usar seu objeto personalizado, assim:

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

Quando você executa o programa, você deve obter a seguinte saída:

microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template

A saída parece melhor agora, graças ao método personalizado Write que você escreveu. Aqui está a versão final do programa:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

Escrever uma API de servidor personalizada

Finalmente, vamos explorar outro caso de uso para interfaces que você pode achar útil se estiver criando uma API de servidor. A maneira típica de escrever um servidor Web é usando a http.Handler interface do pacote, que se parece com o net/http seguinte (você não precisa escrever este código):

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

Observe como a ListenAndServe função está esperando um endereço de servidor, como http://localhost:8000, e uma instância do que despacha Handler a resposta da chamada para o endereço do servidor.

Vamos criar e explorar o seguinte programa:

package main

import (
    "fmt"
    "log"
    "net/http"
)

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Antes de explorarmos o código anterior, vamos executá-lo assim:

go run main.go

Se você não obtiver nenhum resultado, é um bom sinal. Agora abra http://localhost:8000 em uma nova janela do navegador ou, no seu terminal, execute o seguinte comando:

curl http://localhost:8000

Agora você deve obter a seguinte saída:

Go T-Shirt: $25.00
Go Jacket: $55.00

Vamos rever o código anterior lentamente para entender o que ele faz e observar o poder das interfaces Go. Primeiro, você começa criando um tipo personalizado para um float32 tipo, com a ideia de escrever uma implementação personalizada do String() método, que você usa mais tarde.

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

Em seguida, escrevemos a ServeHTTP implementação do método que o http.Handler poderia usar. Observe como criamos um tipo personalizado novamente, mas desta vez é um mapa, não uma estrutura. Em seguida, escrevemos o ServeHTTP método usando o database tipo como recetor. A implementação deste método usa os dados do recetor, faz um loop através dele e imprime cada item.

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

Finalmente, na main() função, instanciamos um database tipo e o inicializamos com alguns valores. Iniciamos o servidor HTTP usando a http.ListenAndServe função, onde definimos o endereço do servidor, incluindo a porta a ser usada e o db objeto que implementa uma versão personalizada do ServeHTTP método. Quando você executa o programa, Go usa sua implementação desse método, e é assim que você usa e implementa uma interface em uma API de servidor.

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Você pode encontrar outro caso de uso para interfaces em uma API de servidor quando você usa a http.Handle função. Para obter mais informações, consulte a postagem Escrevendo aplicativos Web no site Go.