Nhan Nguyen

Understanding interfaces in Golang

Nov 8, 2022 programming golang
image

Interfaces are very familiar in the OOP world, which help us write decoupled, maintainable, and scalable code. Go also have interfaces that have some different characteristics than other languages. Understanding interfaces is essential for writing better Go programs


Definition

In Go, interfaces are abstract types. An interface defines a set of one or more methods that a concrete type want to satisfy that interface must implement. For example,

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

If you have an interface value, you can not know what it is, you can only know what it can do. For example, the function below accepts a parameter of type interface io.Writer

func Foo(w io.Writer, msg string) {
    _, err := w.Write([]byte(msg))
    // error handling...
}

When Foo is invoked, you know nothing about what w is, whether it is a variable of type *os.File or *bytes.Buffer (both implement io.Writer interface). But you can know that it can write a set of bytes, which is the commitment of the Writer interface.

Interfaces satisfaction

Interfaces in Go are satisfied implicitly. You don’t need to explicitly define that your type implement which interfaces like other languages.

interface Sleeper {
  public void Sleep();
}

// we must explicitly say that Pig implements the Sleeper interface
class Pig implements Sleeper {
  public void Sleep() {
    System.out.println("Zzz");
  }
}

In Java, you must explicitly define that class Pig implements the Animal interface.

In Go, to be considered as an instance of an interface, a concrete type only needs to implement all methods defined by that interface.

type Sleeper interface {
    Sleep()
}

// we don't explicitly say that Pig implements the Sleeper interface
type Pig struct{} 

func (Pig) Sleep() {
    fmt.Println("Zzz")
}

func main() {
    var a Sleeper
    a = Pig{} // a value of Pig can be assigned to a Sleeper variable
    a.Sleep()
}

In the program above, Pig implements Sleep method, and its value is automatically considered as an instance of the Sleeper interface.

Only the methods revealed by the interface type may be called, even if the concrete type has others.

type Sleeper interface {
    Sleep()
}

// we don't explicitly say that Pig implements the Sleeper interface
type Pig struct{} 

func (Pig) Sleep() {
    fmt.Println("Zzz")
}
func (Pig) Eat() {
    fmt.Println("Eating")
}

func main() {
    var a Sleeper
    a = Pig{} // a value of Pig can be assigned to a Sleeper variable
    a.Sleep()
    a.Eat() // a.Eat undefined (type Sleeper has no field or method Eat)
}

Empty interface type

Empty interface type or interface{} is an interface type that has no methods at all. There is no method required to satisfy an empty interface type, we can assign any values to the empty interface.

// we can assign anything to a variable of type interface{}
var x interface{}
x = true
x = 12.34
x = "hi"
x = map[string]int{"one": 1}

Of course, we can not do anything with a value of type interface{} since it has no methods. We will learn how to get the concrete value back out in the later section of this post.

Interfaces values

Conceptually, a value of an interface type has two components, a concrete type, and a value of that type, or the interface’s dynamic type and dynamic value. An interface is described as nil or non-nil based on its dynamic type. Consider the program below

func main() {
    var w io.Writer

    w = os.Stdout // os.Stdout's type is *os.File
    w.Write([]byte("hello"))

    w = new(bytes.Buffer) // type *byte.Buffer
    w.Write([]byte("hello"))

    w = nil
    // w.Write([]byte("hello")) // panic: runtime error: invalid memory address or nil pointer dereference
}

First, var w io.Writer we create a nil interface.

The statement w = os.Stdout assign os.Stdout which is of type *os.File to w. w's dynamic type is now *os.File, and its dynamic value holds a copy of os.Stdout.

The statement calling the Write method after that causes the (*os.File).Write to be called and prints “hello” to stdout.

The statement w = new(bytes.Buffer) changes w's dynamic type to *bytes.Buffer and its value to a pointer to the newly allocated buffer.

Now, calling the Write method causes the (*bytes.Buffer).Write to be called and appends “hello” to the buffer.

Finally we assign nil to w. Calling methods from a nil interface value causes a panic.

What do you think is the output of this program?

func main() {
    var (
        b *bytes.Buffer // nil
        w io.Writer     // nil
    )
    
    w = b
    fmt.Println(w == nil)
}

In the example above, w is the nil interface at beginning, has both empty dynamic type and empty dynamic value. When you assign b to w, the dynamic value of w is still nil (which is the value of b), but w's dynamic type is now *bytes.Buffer(this is b’s type). So now w is not a nil interface anymore, or w == nil will be false.

Type assertions

To access the concrete value (dynamic value component) of an interface value, you can use type assertion. A type assertion is an operation applied to an interface value, look like x.(T), with x is an interface value, and T is the concrete type. x.(T) means you want to assert that the dynamic type of x is T.

A failed assertion causes a panic. To prevent panic from a failed assertion, you can use additional variable to indicate whether the assertion is success or not. If success it will return true, otherwise, return false.

Example

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s) // hello
    
    // f = i.(float64) // panic

    s, ok := i.(string)
    fmt.Println(s, ok) // hello true

    f, ok := i.(float64)
    fmt.Println(f, ok) // 0 false
}

Other example

func main() {
    var w io.Writer = new(bytes.Buffer)

    b := w.(*bytes.Buffer)
    fmt.Println(b)

    // f = w.(*os.File) // panic

    b, ok := w.(*bytes.Buffer)
    // you can now also access to b.String() method, which w can't
    fmt.Println(b.String(), ok) 

    f, ok := w.(*os.File)
    fmt.Println(f, ok)
}

Type switches

type switch is a construct that permits several type assertions in series. A type switch is like a regular switch statement, but the cases in a type switch specify types (not values), and those values are compared against the type of the value held by the given interface value.

switch v := i.(type) {
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}

Example

func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
        fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
        fmt.Printf("I don't know about type %T!\n", v)
    }
}

func main() {
    do(21)      // Twice 21 is 42
    do("hello") // "hello" is 5 bytes long
    do(true)    // I don't know about type bool!
}

References