One place for hosting & domains

      Interfaces

      Como usar interfaces em Go


      Introdução

      Escrever um código flexível, reutilizável e modular é vital para o desenvolvimento de programas versáteis. Trabalhar dessa maneira garante que o código seja mais fácil de manter, evitando assim a necessidade de fazer a mesma mudança em vários locais. A maneira de se conseguir isso irá variar de uma linguagem para outra. Por exemplo, a herança é uma abordagem comum usada em linguagens como Java, C++, C#, entre outras.

      Os desenvolvimentos também podem atingir esses mesmos objetivos de design através da composição. A composição é uma maneira de combinar objetos ou tipos de dados em um ambiente mais complexo. Esta é a abordagem que a linguagem Go usa para promover a reutilização, modularidade e flexibilidade dos códigos. As interfaces em Go proporcionam um método de organizar composições complexas. Aprender como usá-las permitirá que você crie um código comum e reutilizável.

      Neste artigo, vamos aprender como compor tipos personalizados que tenham comportamentos comuns, os quais nos permitirão reutilizar o nosso código. Também vamos aprender como implementar interfaces para nossos próprios tipos personalizados que irão atender interfaces definidas de outro pacote.

      Definindo um comportamento

      Uma das implementações principais da composição é o uso das interfaces. Uma interface define um comportamento de um tipo. Uma das interfaces mais comuns usadas na biblioteca padrão do Go é a interface fmt.Stringer:

      type Stringer interface {
          String() string
      }
      

      A primeira linha de código define um type chamado Stringer. Em seguida, ele declara que ele é uma interface. Assim como definir uma struct, o Go usa chaves ({}) para cercar a definição da interface. Em comparação com a definição das structs, definimos apenas o comportamento da interface; ou seja, “o que esse tipo pode fazer”.

      No caso da interface Stringer, o único comportamento é o método String(). O método não aceita argumentos e retorna uma string.

      Em seguida, vamos examinar um código que tem o comportamento fmt.Stringer:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          fmt.Println(a.String())
      }
      

      A primeira coisa que vamos fazer é criar um novo tipo chamado Article. Esse tipo tem um campo Title e um campo Author e ambos são do tipo de dados string:

      main.go

      ...
      type Article struct {
          Title string
          Author string
      }
      ...
      

      Em seguida, definimos um método chamado String no tipo Article. O método String retornará uma string que representa o tipo Article:

      main.go

      ...
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      ...
      

      Então, em nossa função main, criamos uma instância do tipo Article e a atribuímos à variável chamada a. Informamos os valores de "Understanding Interfaces in Go" para o campo Title, e "Sammy Shark" para o campo Author:

      main.go

      ...
      a := Article{
          Title: "Understanding Interfaces in Go",
          Author: "Sammy Shark",
      }
      ...
      

      Em seguida, imprimimos o resultado do método String, chamando fmt.Println e enviando o resultado da chamada do método a.String():

      main.go

      ...
      fmt.Println(a.String())
      

      Após executar o programa, você verá o seguinte resultado:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark.

      Até agora, não usamos uma interface, mas criamos um tipo que tinha um comportamento. Esse comportamento correspondia ao da interface fmt.Stringer. Em seguida, vamos ver como podemos usar aquele comportamento para tornar nosso código mais reutilizável.

      Definindo uma interface

      Agora que temos nosso tipo definido com o comportamento desejado, podemos examinar como usar tal comportamento.

      No entanto, antes de fazer isso, vamos examinar o que precisaríamos fazer se quiséssemos chamar o método String do tipo Article em uma função:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(a Article) {
          fmt.Println(a.String())
      }
      

      Nesse código, adicionamos uma nova função chamada Print, que aceita um Article como um argumento. Note que a única coisa que a função Print faz é chamar o método String. Por conta disso, podemos em vez disso, definir uma interface para enviar para função:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Aqui, criamos uma interface chamada Stringer:

      main.go

      ...
      type Stringer interface {
          String() string
      }
      ...
      

      A interface Stringer tem apenas um método, chamado String() que retorna uma string. Um método é uma função especial com escopo definido para um tipo específico em Go. Ao contrário do que ocorre com uma função, um método só pode ser chamado a partir da instância do tipo em que ele foi definido.

      Em seguida, atualizamos a assinatura do método Print para aceitar um Stringer e não um tipo concreto do Article. Como o compilador sabe que uma interface Stringer define o método String, ele aceitará apenas tipos que também possuem o método String.

      Agora, podemos usar o método Print com qualquer coisa que satisfaça a interface Stringer. Vamos criar outro tipo para demonstrar isso:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title  string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Book struct {
          Title  string
          Author string
          Pages  int
      }
      
      func (b Book) String() string {
          return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title:  "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      
          b := Book{
              Title:  "All About Go",
              Author: "Jenny Dolphin",
              Pages:  25,
          }
          Print(b)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Agora, adicionamos um segundo tipo chamado Book. Ele também tem o método String definido. Isso significa que ele também atende a interface Stringer. Por conta disso, podemos enviá-lo para nossa função Print:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

      Até agora, demonstramos como usar uma única interface apenas. No entanto, uma interface pode ter mais de um comportamento definido. Em seguida, vamos ver como podemos tornar nossas interfaces mais versáteis declarando mais métodos.

      Múltiplos comportamentos em uma interface

      Um dos princípios norteadores para se escrever códigos em Go é escrever tipos pequenos e concisos e que possam entrar em composições com tipos maiores e mais complexos. O mesmo vale quando se compoem interfaces. Para ver como vamos criar uma interface, vamos primeiro começar definindo apenas uma interface. Vamos definir duas formas, um Circle e um Square, e elas definirão um método chamado Area. Esse método retornará a área geométrica de suas formas respectivas:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * math.Pow(c.Radius, 2)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      type Sizer interface {
          Area() float64
      }
      
      func main() {
          c := Circle{Radius: 10}
          s := Square{Height: 10, Width: 5}
      
          l := Less(c, s)
          fmt.Printf("%+v is the smallestn", l)
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      

      Como cada tipo declara o método Area, podemos criar uma interface que define tal comportamento. Criamos a seguinte interface Sizer:

      main.go

      ...
      type Sizer interface {
          Area() float64
      }
      ...
      

      Então, definimos uma função chamada Less que aceita dois Sizer e retorna o menor deles:

      main.go

      ...
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      ...
      

      Note que aceitamos não apenas ambos argumentos como o tipo Sizer, mas que também retornamos o resultado como um Sizer também. Isso significa que não vamos mais retornar um Square ou um Circle, mas sim a interface do Sizer.

      Por fim, imprimimos o que tinha a menor área:

      Output

      {Width:5 Height:10} is the smallest

      Em seguida, vamos adicionar outro comportamento a cada tipo. Desta vez, vamos adicionar o método String() que retorna uma string. Isso irá satisfazer a interface fmt.Stringer:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * math.Pow(c.Radius, 2)
      }
      
      func (c Circle) String() string {
          return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      func (s Square) String() string {
          return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
      }
      
      type Sizer interface {
          Area() float64
      }
      
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      
      func main() {
          c := Circle{Radius: 10}
          PrintArea(c)
      
          s := Square{Height: 10, Width: 5}
          PrintArea(s)
      
          l := Less(c, s)
          fmt.Printf("%v is the smallestn", l)
      
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Como o tipo Circle e o tipo Square implementam tanto os métodos Area como String, podemos agora criar outra interface para descrever aquele conjunto de comportamentos mais amplo. Para tanto, vamos criar uma interface chamada Shaper. Vamos compor isso a partir da interface Sizer e da interface fmt.Stringer:

      main.go

      ...
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      ...
      

      Nota: é considerada como escolha idiomática tentar nomear sua interface de modo a terminar em er, como fmtStringer, io.Writer, etc. É por esse motivo que nomeamos nossa interface como Shaper e não Shape.

      Agora, podemos criar uma função chamada PrintArea, que aceita um Shaper como um argumento. Isso significa que podemos chamar ambos métodos no valor enviado para o método Area e para o método String:

      main.go

      ...
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Se executarmos o programa, vamos receber o seguinte resultado:

      Output

      area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest

      Agora, vimos como podemos criar interfaces menores e compilá-las em interfaces maiores, conforme necessário. Embora pudéssemos ter começado com a interface maior para depois enviá-la para todas as nossas funções, é considerada melhor prática enviar apenas a interface menor para uma função que seja necessária. Isso tipicamente resulta em um código mais claro, uma vez que qualquer um que aceite uma interface menor específica pretende trabalhar somente com aquele comportamento definido.

      Por exemplo, se enviássemos o Shaper para a função Less, poderíamos presumir que ele chamaria os dois métodos: Area e String. No entanto, como pretendemos chamar somente o método Area, a função Less se torna clara, na medida em que sabemos que somente podemos chamar o método Area de um argumento que tiver sido enviado para essa função.

      Conclusão

      Vimos como criar interfaces menores e que compilá-las em interfaces maiores nos permite compartilhar apenas o que precisamos para uma função ou método. Também aprendemos que podemos compor nossas interfaces a partir de outras interfaces, incluindo aquelas definidas de outros pacotes, não apenas nossos pacotes.

      Se quiser aprender mais sobre a linguagem de programação Go, confira toda a série sobre Como codificar em Go.



      Source link

      Cómo usar interfaces en Go


      Introducción

      Escribir código flexible, reutilizable y modular es crucial para desarrollar programas versátiles. Trabajar de esta forma garantiza que se pueda facilitar el mantenimiento del código evitando la necesidad de realizar el mismo cambio en varios puntos. La forma en que esto se consigue varía según el lenguaje. Por ejemplo, inheritance es un enfoque común que se utiliza en lenguajes como Java, C++ y C#, entre otros.

      Los desarrolladores también pueden alcanzar esos objetivos de diseño a través de la composición. La composición es una alternativa para combinar objetos o tipos de datos y formar otros más complejos. Este es el enfoque que Go utiliza para promover la reutilización del código, la modularidad y la flexibilidad. Las interfaces de Go proporcionan un método para organizar composiciones complejas y aprender a usarlas le permitirá crear código común y reutilizable.

      En este artículo, aprenderá a componer tipos personalizados que tienen comportamientos comunes, lo que nos permitirá reutilizar nuestro código. También aprenderá a implementar interfaces para nuestros tipos personalizados propios que satisfarán las interfaces definidas desde otro paquete.

      Definir un comportamiento

      Una de las principales implementaciones de la composición es el uso de interfaces. Una interfaz define un comportamiento de un tipo. Una de las interfaces que más se usan en la biblioteca estándar de Go es fmt.Stringer:

      type Stringer interface {
          String() string
      }
      

      La primera línea de código define un type llamado Stringer. Luego indica que es una interfaz. Al igual cuando se define una struct, Go utiliza llaves ({}) para rodear la definición de la interfaz. En comparación con la definición de estructuras, solo definimos el comportamiento de las interfaces; es decir, “qué puede hacer este tipo”.

      En el caso de la interfaz Stringer, el único comportamiento es el método String(). El método no toma argumentos y muestra una cadena.

      A continuación, veremos código que tiene el comportamiento fmt.Stringer:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          fmt.Println(a.String())
      }
      

      Lo primero que hacemos es crear un nuevo tipo llamado Article. Este tipo tiene un campo Title y un campo Author, y ambos son de la cadena de tipo de datos:

      main.go

      ...
      type Article struct {
          Title string
          Author string
      }
      ...
      

      A continuación, definimos un method llamado String en el tipo Article. El método String mostrará una cadena que representa el tipo Article:

      main.go

      ...
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      ...
      

      A continuación, en nuestra función main, creamos una instancia del tipo Article y la asignamos a la variable llamada a. Proporcionamos los valores de "Understanding Interfaces in Go" para el campo Title y "Sammy Shark"para el campo Author:

      main.go

      ...
      a := Article{
          Title: "Understanding Interfaces in Go",
          Author: "Sammy Shark",
      }
      ...
      

      A continuación, imprimimos el resultado del método String invocando fmt.PrintIn y pasando el resultado de la invocación del método a.String():

      main.go

      ...
      fmt.Println(a.String())
      

      Después de ejecutar el programa, verá el siguiente resultado:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark.

      Hasta ahora no usamos una interfaz, pero creamos un tipo que tuvo un comportamiento. Ese comportamiento coincidió con la interfaz fmt.Stringer. A continuación, veremos la forma de usar ese comportamiento para hacer que nuestro código sea más reutilizable.

      Definir una interfaz

      Ahora que nuestro tipo está definido con el comportamiento deseado, podemos ver la forma de usar ese comportamiento.

      Antes de hacer eso, sin embargo, veremos lo que deberíamos hacer si deseáramos invocar el método String desde el tipo Article en una función:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(a Article) {
          fmt.Println(a.String())
      }
      

      En este código, añadimos una nueva función llamada Print que toma un Article como argumento. Observe que lo único que la función Print hace es invocar el método String. Debido a esto, podríamos definir una interfaz que se pasaría a la función:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Aquí creamos una interfaz llamada Stringer:

      main.go

      ...
      type Stringer interface {
          String() string
      }
      ...
      

      La interfaz Stringer solo tiene un método, llamado String(), que muestra una string. Un método es una función especial que tiene ámbito en un tipo específico en Go. A diferencia de una función, un método solo puede invocarse desde la instancia del tipo sobre el que se definió.

      A continuación actualizamos la firma del método Print para tomar un Stringer y no un tipo concreto de Article. Debido a que el compilador reconoce que una interfaz Stringer define el método String, solo aceptará los tipos que también tienen el método String.

      Ahora podemos usar el método Print con cualquier cosa que se adecue a la interfaz Stringer. Crearemos otro tipo para demostrar esto:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title  string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Book struct {
          Title  string
          Author string
          Pages  int
      }
      
      func (b Book) String() string {
          return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title:  "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      
          b := Book{
              Title:  "All About Go",
              Author: "Jenny Dolphin",
              Pages:  25,
          }
          Print(b)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Ahora, añadimos un segundo tipo llamado Book. También tiene el método String definido. Esto significa que además se adecua a la interfaz Stringer. Debido a esto, podemos enviarlo también a nuestra función Print:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

      Hasta ahora, demostramos la forma de usar una interfaz única. Sin embargo, para una interfaz puede haber más de un comportamiento definido. A continuación, veremos la forma en que podemos hacer que nuestras interfaces sean más versátiles declarando más métodos.

      Varios comportamientos en una interfaz

      Uno de los objetivos principales de escribir código en Go es escribir tipos pequeños y concisos, componerlos de modo que conformen tipos más grandes y complejos. Sucede lo mismo cuando se componen interfaces. Para ver la forma de crear una interfaz, primero comenzaremos definiendo solo una interfaz. Definiremos dos formas, Circle y Square, y ambas definirán un método llamado Area. Este método mostrará el área geométrica de sus respectivas formas:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * math.Pow(c.Radius, 2)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      type Sizer interface {
          Area() float64
      }
      
      func main() {
          c := Circle{Radius: 10}
          s := Square{Height: 10, Width: 5}
      
          l := Less(c, s)
          fmt.Printf("%+v is the smallestn", l)
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      

      Debido a que cada tipo declara el método Area, podemos crear una interfaz que defina ese comportamiento. Crearemos la siguiente interfaz Sizer:

      main.go

      ...
      type Sizer interface {
          Area() float64
      }
      ...
      

      A continuación definiremos una función llamada Less que toma dos Sizer y muestra el más pequeño:

      main.go

      ...
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      ...
      

      Observe que no solo aceptamos ambos argumentos como el tipo Sizer, sino también mostramos el resultado como Sizer. Esto significa que ya no mostramos un Square ni un Circle, sino la interfaz Sizer.

      Por último, imprimimos el que tenía el área más pequeña:

      Output

      {Width:5 Height:10} is the smallest

      A continuación, añadiremos otro comportamiento a cada tipo. Esta vez, añadiremos el método String() que muestra una cadena. Esto satisfará la interfaz fmt.Stringer:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * math.Pow(c.Radius, 2)
      }
      
      func (c Circle) String() string {
          return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      func (s Square) String() string {
          return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
      }
      
      type Sizer interface {
          Area() float64
      }
      
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      
      func main() {
          c := Circle{Radius: 10}
          PrintArea(c)
      
          s := Square{Height: 10, Width: 5}
          PrintArea(s)
      
          l := Less(c, s)
          fmt.Printf("%v is the smallestn", l)
      
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Debido a que los tipos Circle y Square implementan los métodos Area y String, podemos crear otra interfaz para describir ese conjunto más amplio de comportamientos. Para hacer esto, crearemos una interfaz llamada Shaper. Compondremos lo siguiente con las interfaces Sizer y fmt.Stringer:

      main.go

      ...
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      ...
      

      Nota: Se considera que corresponde intentar dar nombre a su interfaz con finalización en er; fmt.Stringer e io.Writer son algunos ejemplos. Por eso, dimos a nuestra interfaz el nombre Shaper y no Shape.

      Ahora podemos crear una función llamada PrintArea que toma Shaper como argumento. Esto significa que podemos invocar ambos métodos en el valor pasado para los métodos Area y String:

      main.go

      ...
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Si ejecutamos el programa, veremos el siguiente resultado:

      Output

      area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest

      Acabamos de ver la forma de podemos crear interfaces más pequeñas y hacerlas más grandes según sea necesario. Aunque podríamos haber comenzado con la interfaz más grande y haberla pasado a todas nuestras funciones, se considera mejor enviar solo la interfaz más pequeña a una función que sea necesaria. Esto normalmente da como resultado un código más claro, ya que cualquier cosa que acepte una interfaz específica más pequeña solo tiene la intención de funcionar con ese comportamiento definido.

      Por ejemplo, si pasamos Shaper a la función Less podemos suponer que invocará los métodos Area y String. Sin embargo, ya que solo queremos invocar el método Area, hace que la función Less sea clara porque sabemos que solo podemos invocar el método Area de cualquier argumento que se le pase.

      Conclusión

      Hemos visto que la creación de interfaces más pequeñas y su ampliación nos permite compartir solo lo que necesitamos con una función o un método. También aprendió que se pueden componer nuestras interfaces a partir de otras, incluidas aquellas definidas desde otros paquetes, no solo los nuestros paquetes.

      Si desea obtener más información acerca del lenguaje de programación de Go, consulte toda la serie sobre Cómo codificar en Go.



      Source link

      How To Use Interfaces in Go


      Introduction

      Writing flexible, reusable, and modular code is vital for developing versatile programs. Working in this way ensures code is easier to maintain by avoiding the need to make the same change in multiple places. How you accomplish this varies from language to language. For instance, inheritance is a common approach that is used in languages such as Java, C++, C#, and more.

      Developers can also attain those same design goals through composition. Composition is a way to combine objects or data types into more complex ones. This is the approach that Go uses to promote code reuse, modularity, and flexibility. Interfaces in Go provide a method of organizing complex compositions, and learning how to use them will allow you to create common, reusable code.

      In this article, we will learn how to compose custom types that have common behaviors, which will allow us to reuse our code. We’ll also learn how to implement interfaces for our own custom types that will satisfy interfaces defined from another package.

      Defining a Behavior

      One of the core implementations of composition is the use of interfaces. An interface defines a behavior of a type. One of the most commonly used interfaces in the Go standard library is the fmt.Stringer interface:

      type Stringer interface {
          String() string
      }
      

      The first line of code defines a type called Stringer. It then states that it is an interface. Just like defining a struct, Go uses curly braces ({}) to surround the definition of the interface. In comparison to defining structs, we only define the interface’s behavior; that is, “what can this type do”.

      In the case of the Stringer interface, the only behavior is the String() method. The method takes no arguments and returns a string.

      Next, let’s look at some code that has the fmt.Stringer behavior:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          fmt.Println(a.String())
      }
      

      The first thing we do is create a new type called Article. This type has a Title and an Author field and both are of the string data type:

      main.go

      ...
      type Article struct {
          Title string
          Author string
      }
      ...
      

      Next, we define a method called String on the Article type. The String method will return a string that represents the Article type:

      main.go

      ...
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      ...
      

      Then, in our main function, we create an instance of the Article type and assign it to the variable called a. We provide the values of "Understanding Interfaces in Go" for the Title field, and "Sammy Shark" for the Author field:

      main.go

      ...
      a := Article{
          Title: "Understanding Interfaces in Go",
          Author: "Sammy Shark",
      }
      ...
      

      Then, we print out the result of the String method by calling fmt.Println and passing in the result of the a.String() method call:

      main.go

      ...
      fmt.Println(a.String())
      

      After running the program you’ll see the following output:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark.

      So far, we haven’t used an interface, but we did create a type that had a behavior. That behavior matched the fmt.Stringer interface. Next, let’s see how we can use that behavior to make our code more reusable.

      Defining an Interface

      Now that we have our type defined with the desired behavior, we can look at how to use that behavior.

      Before we do that, however, let’s look at what we would need to do if we wanted to call the String method from the Article type in a function:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(a Article) {
          fmt.Println(a.String())
      }
      

      In this code we add a new function called Print that takes an Article as an argument. Notice that the only thing the Print function does is call the String method. Because of this, we could instead define an interface to pass to the function:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Here we created an interface called Stringer:

      main.go

      ...
      type Stringer interface {
          String() string
      }
      ...
      

      The Stringer interface has only one method, called String() that returns a string. A method is a special function that is scoped to a specific type in Go. Unlike a function, a method can only be called from the instance of the type it was defined on.

      We then update the signature of the Print method to take a Stringer, and not a concrete type of Article. Because the compiler knows that a Stringer interface defines the String method, it will only accept types that also have the String method.

      Now we can use the Print method with anything that satisfies the Stringer interface. Let’s create another type to demonstrate this:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title  string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Book struct {
          Title  string
          Author string
          Pages  int
      }
      
      func (b Book) String() string {
          return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title:  "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      
          b := Book{
              Title:  "All About Go",
              Author: "Jenny Dolphin",
              Pages:  25,
          }
          Print(b)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      We now add a second type called Book. It also has the String method defined. This means it also satisfies the Stringer interface. Because of this, we can also send it to our Print function:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

      So far, we have demonstrated how to use just a single interface. However, an interface can have more than one behavior defined. Next, we’ll see how we can make our interfaces more versatile by declaring more methods.

      Multiple Behaviors in an Interface

      One of the core tenants of writing Go code is to write small, concise types and compose them up to larger, more complex types. The same is true when composing interfaces. To see how we build up an interface, we’ll first start by defining only one interface. We’ll define two shapes, a Circle and Square, and they will both define a method called Area. This method will return the geometric area of their respective shapes:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * c.Radius
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      type Sizer interface {
          Area() float64
      }
      
      func main() {
          c := Circle{Radius: 10}
          s := Square{Height: 10, Width: 5}
      
          l := Less(c, s)
          fmt.Printf("%+v is the smallestn", l)
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      

      Because each type declares the Area method, we can create an interface that defines that behavior. We create the following Sizer interface:

      main.go

      ...
      type Sizer interface {
          Area() float64
      }
      ...
      

      We then define a function called Less that takes two Sizer and returns the smallest one:

      main.go

      ...
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      ...
      

      Notice that we not only accept both arguments as the type Sizer, but we also return the result as a Sizer as well. This means that we no longer return a Square or a Circle, but the interface of Sizer.

      Finally, we print out what had the smallest area:

      Output

      {Radius:10} is the smallest

      Next, let’s add another behavior to each type. This time we’ll add the String() method that returns a string. This will satisfy the fmt.Stringer interface:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * c.Radius
      }
      
      func (c Circle) String() string {
          return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      func (s Square) String() string {
          return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
      }
      
      type Sizer interface {
          Area() float64
      }
      
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      
      func main() {
          c := Circle{Radius: 10}
          PrintArea(c)
      
          s := Square{Height: 10, Width: 5}
          PrintArea(s)
      
          l := Less(c, s)
          fmt.Printf("%v is the smallestn", l)
      
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Because both the Circle and the Square type implement both the Area and String methods, we can now create another interface to describe that wider set of behavior. To do this, we’ll create an interface called Shaper. We’ll compose this of the Sizer interface and the fmt.Stringer interface:

      main.go

      ...
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      ...
      

      Note: It is considered idiomatic to try to name your interface by ending in er, such as fmt.Stringer, io.Writer, etc. This is why we named our interface Shaper, and not Shape.

      Now we can create a function called PrintArea that takes a Shaper as an argument. This means that we can call both methods on the passed in value for both the Area and String method:

      main.go

      ...
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      If we run the program, we will receive the following output:

      Output

      area of Circle {Radius: 10.00} is 31.42 area of Square {Width: 5.00, Height: 10.00} is 50.00 Circle {Radius: 10.00} is the smallest

      We have now seen how we can create smaller interfaces and build them up into larger ones as needed. While we could have started with the larger interface and passed it to all of our functions, it is considered best practice to send only the smallest interface to a function that is needed. This typically results in clearer code, as anything that accepts a specific smaller interface only intends to work with that defined behavior.

      For example, if we passed Shaper to the Less function, we may assume that it is going to call both the Area and String methods. However, since we only intend to call the Area method, it makes the Less function clear as we know that we can only call the Area method of any argument passed to it.

      Conclusion

      We have seen how creating smaller interfaces and building them up to larger ones allows us to share only what we need to a function or method. We also learned that we can compose our interfaces from other interfaces, including those defined from other packages, not just our packages.

      If you’d like to learn more about the Go programming language, check out the entire How To Code in Go series.



      Source link