Usar interfaces em Go
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 Writer
notará 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.