What Makes Golang Go

The Power of Go Interfaces

4 May 2021

Ricardo Gerardi

Powerful Command-Line Applications in Go, Author

Senior Automation Consultant, Red Hat

Go and CLI Apps

  • Simplicity
  • Reliability of statically typed languages with flexibility of dynamic languages
  • No runtime dependencies
  • Rapid prototyping
  • Built-in concurrency
  • Multiplatform and cross compilation
rggo
Go Gopher (CC BY 3.0) original design by Renee French
2

Less is Exponentially More

There are no:

3

Interfaces

4

Interfaces

A way to enable seemingly incompatible entities to interact

5

Interfaces in Go

The Abstract Type

6

Concrete Types

var power int

type Jedi struct {
    FirstName string
    LastName  string
    Age       int
    Rank      string
}

func foo() {
    name := "Luke"    // string
}
7

Defining Behaviour (Methods)

type Jedi struct {
    FirstName string
    LastName  string
    Age       int
    Rank      string
}

func (j Jedi) ForcePush(p int) int {
    // TODO Method implementation
    return p
}

type power int

func (p power) unleash() {
    // TODO Method implementation
}
8

Defining Interfaces

type ForceUser interface {
    ForcePush(int) int
}
9

Example

package main

import "fmt"

type Jedi struct {
    FirstName string
    LastName  string
    Age       int
    Rank      string
}

func main() {
    luke := Jedi{
        FirstName: "Luke",
        LastName:  "Skywalker",
        Age:       22,
        Rank:      "Master",
    }

    fmt.Println(luke)
}
10

Example (cont.)

package main

import "fmt"

type Jedi struct {
    FirstName string
    LastName  string
    Age       int
    Rank      string
}

func (j Jedi) String() string {
    return j.FirstName + " " + j.LastName
}

func main() {
    luke := Jedi{
        FirstName: "Luke",
        LastName:  "Skywalker",
        Age:       22,
        Rank:      "Master",
    }

    fmt.Println(luke)
}
11

fmt.Stringer Interface

// Stringer is implemented by any value that has a String method,
// which defines the ``native'' format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
    String() string
}
12

Implicit Satisfaction - No dependencies

package force

type Jedi struct {
    FirstName string
    LastName  string
    Age       int
    Rank      string
}

func (j Jedi) String() string {
    return j.FirstName + " " + j.LastName
}
13

Implicit Satisfaction - No dependencies (cont.)

package main

import (
    "fmt"

    "github.com/rgerardi/philly-ete-2021/print3/force"
)

func main() {
    luke := force.Jedi{
        FirstName: "Luke",
        LastName:  "Skywalker",
        Age:       22,
        Rank:      "Master",
    }

    fmt.Println(luke)
}
14

Small interfaces - powerful abstractions

package main

import (
    "fmt"
    "io/ioutil"
    "os"
)

func main() {
    fmt.Println("Writing data")
    data := []byte("Some data to write")

    f, _ := ioutil.TempFile("/tmp", "data")
    defer f.Close()

    save(f, data)
}

func save(f *os.File, data []byte) error {
    _, err := f.Write(data)
    return err
}
15

io.Writer interface

// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
    Write(p []byte) (n int, err error)
}
16

Require only what you need

package main

import (
    "fmt"
    "io"
    "io/ioutil"
)

func main() {
    fmt.Println("Writing data")
    data := []byte("Some data to write")

    f, _ := ioutil.TempFile("/tmp", "data")
    defer f.Close()

    save(f, data)
}

func save(w io.Writer, data []byte) error {
    _, err := w.Write(data)
    return err
}
17

Small interfaces - Flexible implementation

package main

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

func main() {
    ln, err := net.Listen("tcp", ":3000")
    if err != nil {
        os.Exit(1)
    }
    fmt.Println("Listening on port 3000")
    for {
        conn, err := ln.Accept()
        if err != nil {
            os.Exit(1)
        }
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    msg, err := ioutil.ReadAll(conn)
    if err != nil {
        os.Exit(1)
    }
    fmt.Printf("%s\n", msg)
    conn.Close()
}
18

Small interfaces - Flexible implementation

package main

import (
    "fmt"
    "io"
    "net"
)

func main() {
    fmt.Println("Writing data")
    data := []byte("Some data to write")

    client, _ := net.Dial("tcp", ":3000")
    defer client.Close()

    save(client, data)
}

func save(w io.Writer, data []byte) error {
    _, err := w.Write(data)
    return err
}
19

Small interfaces - Extensive usage

package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
    fmt.Println("Writing data")
    data := []byte("Some data to write")

    client, _ := net.Dial("tcp", ":3000")
    defer client.Close()

    save(LogWriter(client), data)
}

type logWriter struct {
    writer io.Writer
}

func (l logWriter) Write(p []byte) (n int, err error) {
    fmt.Printf("%s\n", p)
    return l.writer.Write(p)
}

func LogWriter(w io.Writer) io.Writer {
    return logWriter{w}
}

func save(w io.Writer, data []byte) error {
	_, err := w.Write(data)
	return err
}
20

Testing

21

How to test this code?

func save(f *os.File, data []byte) error {
    _, err := f.Write(data)
    return err
}
func Test_Save(t *testing.T) {
    testdata := "Test data"
    f, err := ioutil.TempFile("/tmp", "data")
    if err != nil {
        t.Fatal("Error creating test file")
    }
    defer os.Remove(f.Name())

    if err := save(f, []byte(testdata)); err != nil {
        t.Fatal("Error saving data")
    }
    f.Close()

    data, _ := ioutil.ReadFile(f.Name())
    if string(data) != testdata {
        t.Errorf("Expected %s, got %s\n", testdata, string(data))
    }
}
22

Interfaces Improve Testing

func save(w io.Writer, data []byte) error {
    _, err := w.Write(data)
    return err
}
func Test_Save(t *testing.T) {
    buf := &bytes.Buffer{}
    testdata := "Test data"

    if err := save(buf, []byte(testdata)); err != nil {
        t.Fatal("Error saving data")
    }

    if buf.String() != testdata {
        t.Errorf("Expected %s, got %s\n", testdata, buf.String())
    }
}
=== RUN   Test_Save
--- PASS: Test_Save (0.00s)
PASS
ok      github.com/rgerardi/philly-ete-2021/testing/ex2 0.006s
23

Duck Typing

24

Duck Typing

"If it walks like a duck and it quacks like a duck, then it must be a duck"

In Go:

"If it walks like a duck and it quacks like a duck, then it BEHAVES like a duck"

25

Example

package shape

import "math"

type Circle struct {
    Radius float64
}

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

type Rectangle struct {
    Length float64
    Width  float64
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Length + r.Width)
}
26

Example (cont.)

package main

import (
    "fmt"

    "github.com/rgerardi/philly-ete-2021/shape/shape"
)

type border interface {
    Perimeter() float64
}

func main() {
    c := shape.Circle{5}
    r := shape.Rectangle{20, 10}

    printBorder(c)
    printBorder(r)
}

func printBorder(b border) {
    fmt.Println("Border length is", b.Perimeter())
}
27

Composition

package main

import (
	"fmt"

	"github.com/rgerardi/philly-ete-2021/shape/shape"
)

type border interface {
    Perimeter() float64
}

type square struct {
    shape.Rectangle
}

func newSquare(side float64) square {
    return square{shape.Rectangle{side, side}}
}

func main() {
    c := shape.Circle{5}
    r := shape.Rectangle{20, 10}
    s := newSquare(5)

    printBorder(c)
    printBorder(r)
    printBorder(s)
}
func printBorder(b border) {
	fmt.Println("Border length is", b.Perimeter())
}
28

Composition is not inheritance

package main

import (
	"fmt"

	"github.com/rgerardi/philly-ete-2021/shape/shape"
)

type border interface {
    Perimeter() float64
}

type square struct {
    shape.Rectangle
}

func newSquare(side float64) square {
    return square{shape.Rectangle{side, side}}
}

func main() {
    shapes := make([]shape.Rectangle, 3)

    shapes[0] = shape.Rectangle{10, 5}
    shapes[1] = newSquare(5)
}
func printBorder(b border) {
	fmt.Println("Border length is", b.Perimeter())
}
29

Grouping through behaviour

package main

import (
	"fmt"

	"github.com/rgerardi/philly-ete-2021/shape/shape"
)

type border interface {
    Perimeter() float64
}

type square struct {
    shape.Rectangle
}

func newSquare(side float64) square {
    return square{shape.Rectangle{side, side}}
}

func main() {
    shapes := make([]border, 3)

    shapes[0] = shape.Rectangle{10, 5}
    shapes[1] = newSquare(5)
    shapes[2] = shape.Circle{3}

    for _, s := range shapes {
        printBorder(s)
    }
}
func printBorder(b border) {
	fmt.Println("Border length is", b.Perimeter())
}
30

Augmenting

package shape
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (r Rectangle) Area() float64 {
    return r.Length * r.Width
}
31

You can only call methods from interface

package main

import (
	"fmt"

	"github.com/rgerardi/philly-ete-2021/shape/shape"
)

type border interface {
    Perimeter() float64
}

type square struct {
    shape.Rectangle
}

func newSquare(side float64) square {
    return square{shape.Rectangle{side, side}}
}

func main() {
    shapes := make([]border, 3)
    shapes[0] = shape.Rectangle{10, 5}
    shapes[1] = newSquare(5)
    shapes[2] = shape.Circle{3}
    for _, s := range shapes {
        printBorder(s)
        fmt.Println(s.Area())
    }
}
func printBorder(b border) {
	fmt.Println("Border length is", b.Perimeter())
}
32

Composing Interfaces

package main

import (
	"fmt"

	"github.com/rgerardi/philly-ete-2021/shape/shape"
)

type border interface {
	Perimeter() float64
}

type square struct {
	shape.Rectangle
}

func newSquare(side float64) square {
	return square{shape.Rectangle{side, side}}
}

type surface interface {
    Area() float64
}

type surfaceBorder interface {
    border
    surface
}

func main() {
    shapes := make([]surfaceBorder, 3)
    shapes[0] = shape.Rectangle{10, 5}
    shapes[1] = newSquare(5)
    shapes[2] = shape.Circle{3}
    for _, s := range shapes {
        printBorder(s)
        printArea(s)
    }
}

func printArea(s surface) {
    fmt.Println("Surface area is", s.Area())
}
func printBorder(b border) {
	fmt.Println("Border length is", b.Perimeter())
}
33

Extra

34

Type Assertions

package main

import (
	"fmt"

	"github.com/rgerardi/philly-ete-2021/shape/shape"
)

type border interface {
	Perimeter() float64
}

type square struct {
	shape.Rectangle
}

func newSquare(side float64) square {
	return square{shape.Rectangle{side, side}}
}

type surface interface {
	Area() float64
}

type surfaceBorder interface {
	border
	surface
}

func main() {
    shapes := make([]surfaceBorder, 3)
    shapes[0] = shape.Rectangle{10, 5}
    shapes[1] = newSquare(5)
    shapes[2] = shape.Circle{3}
    for _, s := range shapes {
        printBorder(s)
        printArea(s)

        if c, ok := s.(shape.Circle); ok {
            fmt.Println("Radius is:", c.Radius)
        }
    }
}

func printArea(s surface) {
	fmt.Println("Surface area is", s.Area())
}

func printBorder(b border) {
	fmt.Println("Border length is", b.Perimeter())
}
35

Type Switch

package main

import (
	"fmt"

	"github.com/rgerardi/philly-ete-2021/shape/shape"
)

type border interface {
	Perimeter() float64
}

type square struct {
	shape.Rectangle
}

func newSquare(side float64) square {
	return square{shape.Rectangle{side, side}}
}

type surface interface {
	Area() float64
}

type surfaceBorder interface {
	border
	surface
}

func main() {
    shapes := make([]surfaceBorder, 3)
    shapes[0] = shape.Rectangle{10, 5}
    shapes[1] = newSquare(5)
    shapes[2] = shape.Circle{3}
    for _, s := range shapes {
        switch c := s.(type) {
        case shape.Circle:
            fmt.Println("Radius is:", c.Radius)
        case shape.Rectangle:
            fmt.Println("Length is:", c.Length)
        default:
            fmt.Println("Not a rectangle or circle")
        }
    }
}

func printArea(s surface) {
	fmt.Println("Surface area is", s.Area())
}

func printBorder(b border) {
	fmt.Println("Border length is", b.Perimeter())
}
36

Empty Interface: interface{}

"The empty interface says nothing" (Rob Pike)

37

Learn More

38

Summary

39

Powerful Command-Line Applications in Go

40

Thank you

Ricardo Gerardi

Powerful Command-Line Applications in Go, Author

Senior Automation Consultant, Red Hat