2.6 Interface

Interface

Uma das características de projeto mais sutil em Go são interfaces. Depois de ler esta seção, você provavelmente ficará impressionado com sua implementação.

O que é uma interface

Resumidamente, uma interface é um conjunto de métodos que usamos para definir um conjunto de ações.

Como os exemplos nas seções anteriores, tanto Student (Estudante) quanto Employee (Empregado) podem SayHi() (DigaOi()), mas não fazem a mesma coisa.

Vamos fazer mais trabalho. Vamos adicionar mais um método Sing() (Cantar()) para eles, juntamente com o método BorrowMoney() (EmprestarDinheiro()) para Student e SpendSalary() (GastarSalario()) para Employee.

Agora, Student tem três métodos chamados SayHi(), Sing() e BorrowMoney(), e Employee tem SayHi(), Sing() e SpendSalary().

Esta combinação de métodos é chamada de interface e é implementada por ambos Student e Employee. Então, Student e Employee implementam a interface: SayHi() e Sing(). Ao mesmo tempo, Employee não implementa a interface: SayHi(), Sing(), BorrowMoney(), e Student não implementa a interface: SayHi(), Sing(), SpendSalary(). Isso ocorre porque Employee não tem o método BorrowMoney() e Student não tem o método SpendSalary().

Tipo de Interface

Uma interface define um conjunto de métodos, portanto, se um tipo implementa todos os métodos, dizemos que ele implementa a interface.

type Human struct {
    name  string
    age   int
    phone string
}

type Student struct {
    Human
    school string
    loan   float32
}

type Employee struct {
    Human
    company string
    money   float32
}

func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func (h *Human) Sing(lyrics string) {
    fmt.Println("La la, la la la, la la la la la...", lyrics)
}

func (h *Human) Guzzle(beerStein string) {
    fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

// Employee sobrecarrega Sayhi
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) // Sim, você pode dividir em 2 linhas aqui.
}

func (s *Student) BorrowMoney(amount float32) {
    s.loan += amount // (novamente e novamente e...)
}

func (e *Employee) SpendSalary(amount float32) {
    e.money -= amount // Mais vodka por favor!!! Para aguentar o dia! (More vodka please!!! Get me through the day!)
}

// define interface
type Men interface {
    SayHi()
    Sing(lyrics string)
    Guzzle(beerStein string)
}

type YoungChap interface {
    SayHi()
    Sing(song string)
    BorrowMoney(amount float32)
}

type ElderlyGent interface {
    SayHi()
    Sing(song string)
    SpendSalary(amount float32)
}

Sabemos que uma interface pode ser implementada por qualquer tipo, e um tipo pode implementar muitas interfaces simultaneamente.

Observe que qualquer tipo implementa a interface vazia interface{} porque ela não tem nenhum método e todos os tipos possuem zero métodos por padrão.

Valor da interface

Então, que tipo de valores podem ser colocados na interface? Se definirmos uma variável como uma interface de tipo, qualquer tipo que implemente a interface pode ser atribuído a essa variável.

Como no exemplo acima, se definirmos uma variável "m" como interface Men, então qualquer Student, Human ou Employee pode ser atribuído a "m". Então nós poderíamos ter um slice de Men, e qualquer tipo que implemente a interface Men pode atribuir a este slice. Lembre-se no entanto que o slice da interface não tem o mesmo comportamento de uma slice de outros tipos.

package main

import "fmt"

type Human struct {
    name  string
    age   int
    phone string
}

type Student struct {
    Human
    school string
    loan   float32
}

type Employee struct {
    Human
    company string
    money   float32
}

func (h Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func (h Human) Sing(lyrics string) {
    fmt.Println("La la la la...", lyrics)
}

func (e Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) // Sim, você pode dividir em 2 linhas aqui.
}

// Interface Men implementada por Human, Student e Employee
type Men interface {
    SayHi()
    Sing(lyrics string)
}

func main() {
    mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}

    // define interface i
    var i Men

    // posso armazenar Student
    i = mike
    fmt.Println("This is Mike, a Student:")
    i.SayHi()
    i.Sing("November rain")

    // posso armazenar Employee
    i = tom
    fmt.Println("This is Tom, an Employee:")
    i.SayHi()
    i.Sing("Born to be wild")

    // slice de Men
    fmt.Println("Let's use a slice of Men and see what happens")
    x := make([]Men, 3)
    // Estes três elementos são de tipos diferentes, mas todos eles implementam a interface Men
    x[0], x[1], x[2] = paul, sam, mike

    for _, value := range x {
        value.SayHi()
    }
}

Uma interface é um conjunto de métodos abstratos, e pode ser implementada por tipos não interface. Não pode, portanto, implementar-se.

Interface vazia

Uma interface vazia é uma interface que não contém nenhum método, portanto, todos os tipos implementam uma interface vazia. Esse fato é muito útil quando queremos armazenar todos os tipos em algum momento, e é semelhante ao void* em C.

// define a como interface vazia
var a interface{}
var i int = 5
s := "Hello world"
// a pode armazenar valor de qualquer tipo
a = i
a = s

Se uma função usa uma interface vazia como seu tipo de argumento, ela pode aceitar qualquer tipo; Se uma função usa a interface vazia como seu tipo de valor de retorno, ela pode retorna qualquer tipo.

Argumentos de métodos de uma interface

Qualquer variável pode ser usada em uma interface. Então, como podemos usar esse recurso para passar qualquer tipo de variável para uma função?

Por exemplo, usamos muito fmt.Println, mas você já notou que ele pode aceitar qualquer tipo de argumento? Olhando para o código aberto de fmt, vemos a seguinte definição.

type Stringer interface {
        String() string
}

Isso significa que qualquer tipo que implemente a interface Stringer pode ser passada para fmt.Println como um argumento. Vamos provar isso.

package main

import (
    "fmt"
    "strconv"
)

type Human struct {
    name  string
    age   int
    phone string
}

// Human implementa fmt.Stringer
func (h Human) String() string {
    return "Name:" + h.name + ", Age:" + strconv.Itoa(h.age) + " years, Contact:" + h.phone
}

func main() {
    Bob := Human{"Bob", 39, "000-7777-XXX"}
    fmt.Println("This Human is : ", Bob)
}

Olhando para o exemplo anterior de Box (Caixa), você verá que Color (Cor) implementa a interface Stringer também, assim que somos capazes de personalizar o formato de impressão. Se não implementarmos essa interface, fmt.Println imprime o tipo com seu formato padrão.

fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())

Atenção: Se o tipo implementa a interface error, fmt chamará error(), então você não tem que implementar Stringer neste momento.

Tipo de variável em uma interface

Se uma variável é o tipo que implementa uma interface, sabemos que qualquer outro tipo que implementa a mesma interface pode ser atribuído a essa variável. A questão é como podemos saber o tipo específico armazenado na interface. Há duas maneiras que vou lhe mostrar.

  • Declaração do padrão Comma-ok (Vírgula-ok)

Go possui a sintaxe value, ok := element.(T). Isso verifica se a variável é o tipo que esperamos, onde "value" (valor) é o valor da variável, "ok" é uma variável de tipo booleano, "element" (elemento) é a variável da interface e T é o tipo da declaração.

Se o elemento é o tipo que esperamos, ok será verdadeiro, e falso caso contrário.

Vamos usar um exemplo para ver mais claramente.

package main

import (
    "fmt"
    "strconv"
)

type Element interface{}
type List []Element

type Person struct {
    name string
    age  int
}

func (p Person) String() string {
    return "(name: " + p.name + " - age:     " + strconv.Itoa(p.age) + " years)"
}

func main() {
    list := make(List, 3)
    list[0] = 1       // um int (inteiro)
    list[1] = "Hello" // uma string (cadeia de caracteres)
    list[2] = Person{"Dennis", 70}

    for index, element := range list {
        if value, ok := element.(int); ok {
            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
        } else if value, ok := element.(string); ok {
            fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
        } else if value, ok := element.(Person); ok {
            fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
        } else {
            fmt.Printf("list[%d] is of a different type\n", index)
        }
    }
}

É muito fácil usar esse padrão, mas se tivermos muitos tipos para testar, é melhor usar switch.

  • teste de comutação (switch test)

Vamos usar switch para reescrever o exemplo acima.

package main

import (
    "fmt"
    "strconv"
)

type Element interface{}
type List []Element

type Person struct {
    name string
    age  int
}

func (p Person) String() string {
    return "(name: " + p.name + " - age: " + strconv.Itoa(p.age) + " years)"
}

func main() {
    list := make(List, 3)
    list[0] = 1       // um int (inteiro)
    list[1] = "Hello" // uma string (cadeia de caracteres)
    list[2] = Person{"Dennis", 70}

    for index, element := range list {
        switch value := element.(type) {
        case int:
            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
        case string:
            fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
        case Person:
            fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
        default:
            fmt.Println("list[%d] is of a different type", index)
        }
    }
}

Uma coisa que você deve lembrar é que element.(type) não pode ser usado fora do corpo do switch, o que significa que nesse caso você tem que usar o padrão comma-ok.

Interfaces incorporadas (embedded interfaces)

A coisa mais bonita é que Go tem várias sintaxes lógicas incorporadas, como campos anônimos em estruturas. Não supreendentemente, podemos usar interfaces como campos anônimos também, mas chamamos-lhes de Embedded interfaces (interfaces incorporadas). Aqui, seguimos as mesmas regras que campos anônimos. Mais especificamente, se uma interface tiver outra interface incorporada no seu interior, isso será como se ela possuísse todos os métodos que a interface incorporada tem.

Podemos ver que o arquivo fonte em container/heap tem a seguinte definição:

type Interface interface {
        sort.Interface // sort.Interface incorporado
        Push(x interface{}) // um método Push para empurrar elementos para a pilha
        Pop() interface{} // um método Pop para remover elementos da pilha
}

Vemos que sort.Interface é uma interface incorporada, então a interface acima tem os três métodos contidos em sort.Interface implicitamente.

type Interface interface {
        // Len é o número de elementos na coleção.
        Len() int
        // Less retorna se o elemento com índice i deve ser ordenado
        // antes do elemento com o índice j.
        Less(i, j int) bool
        // Swap troca os elementos com índices i e j.
        Swap(i, j int)
}

Outro exemplo é o io.ReadWriter no pacote io.

// io.ReadWriter
type ReadWriter interface {
        Reader
        Writer
}

Reflection

Reflection em Go é usado para determinar informações em tempo de execução. Usamos o pacote reflect, e este artigo oficial explica como reflect funciona em Go.

Há três etapas envolvidas quando usamos reflect. Primeiro, precisamos converter um interface para tipos reflect (reflect.Type ou reflect.Value, isso depende da situação).

t := reflect.TypeOf(i)    // recebe meta-dados no tipo i, e usa t para receber todos os elementos
v := reflect.ValueOf(i)   // recebe o valor atual no tipo i, e usa v para alterar seu valor

Depois disso, podemos converter os tipos reflect para obter os valores que precisamos.

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

Finalmente, se quisermos mudar os valores dos tipos reflect, precisamos torná-los modificáveis. Como discutido anteriormente, há uma diferença entre passar por valor e passar por referência. O código a seguir não compilará.

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

Em vez disso, precisamos usar o seguinte código para alterar os valores dos tipos reflect.

var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

Acabamos de discutir os fundamentos de reflect, no entanto você deve praticar mais para entender mais.

results matching ""

    No results matching ""