Go - Use methods and interfaces in Go (II)

Source: Use methods and interfaces in Go

Use interfaces in Go

Interfaces in Go are a type of data that’s used to represent the behavior of other types. An interface is like a blueprint or a contract that an object should satisfy

Declare an interface

An interface in Go is an abstract type that includes a set of methods.

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

The Shape interface means that any type that you want to consider a Shape needs to have both the Perimeter() and Area() methods. E.g: when you create a Square struct, it has to implement both methods.

Implement an interface

Go don’t have a keyword to implement an interface. An interface in Go is satisfied implicitly by a type when it has all the methods that an interface requires.

type Square struct {
    size float64
}

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

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

Notice how the method’s signature of the Square struct matches the signature of the Shape interface.

Let’s create another type, such as Circle:

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
}

Both Square and Circle have the same methods as the Shape interface, so we can not define more generic functions:

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

// In the code we can use it as
var s Shape = Square{3}
printInformation(s) // main.Square, Area:  9, Perimeter: 12

// or without specifying the type
c:= Circle{6}
printInformation(c) // main.Circle, Area:  113.09733552923255, Perimeter: 37.69911184307752

The beauty of using interfaces is that, for every new type or implementation of Shape, the printInformation function doesn’t have to change.

Examples

Implement a Stringer interface

A example of extending existing functionality is to use a Stringer, which is an interface that has a String() method, like this:

type Stringer interface {
    String() string // String() is the method that will be called when custom print
}

Now, we can crete a new String() public method for our struct

package main

import "fmt"

type Person struct {
    Name, Country string
}

// we create a custom String() method for our Person struct
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)
    // John Doe is from USA
    // Mark Collins is from United Kingdom
}

We use a custom type to write a custom version of the String() method.

Extend an existing implementation

We have this code and we want to extend it by writing a custom implementation of Write() method, that’s in charge of manipulating some data.

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=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

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

Output example:

[{"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
....

We want to modify what’s is printed out from os.Stdout. That’s part of the io package.

Checking the Copy method definition func Copy(dst Writer, src Reader) (written int64, err error), meaning we have to check the dst Writer parameter. But the Writer is a interface:

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

So we can manipulate the Writer interface and create a new custom Write method for our os.Stdout.

First, we need to implement a custom interface, like a empty struct where we apply the new Write method:

// empty struct where wer are going to write the interface
type customWriter struct{}

// struct to parse the API response
type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

// new Write method for our customWriter struct
func (w customWriter) Write(p []byte) (n int, err error) {
  // create an empty struct only for the full_name field
  var resp GitHubResponse
  // unmarshal the response into the struct
  json.Unmarshal(p, &resp)
  for _, r := range resp {
      fmt.Println(r.FullName)
  }
  return len(p), nil
}

So, using the new interface:

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)
}

Write a custom server API

The typical way of writing a web server is by using the http.Handler interface from the net/http package.

package http

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

func ListenAndServe(address string, h Handler) error

Notice how the ListenAndServe function is expecting a server address, such as http://localhost:8000, and an instance of the Handler that will dispatch the response from the call to the server address.

E.g:

package main

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

// create a custom type
type dollars float32

// Implement the String for the new custom type
func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

// custom type
type database map[string]dollars

// use the database as receiver
// uses the data from the receiver, loops through it and prints out each item
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))
}