Interfaces in Go (Part-2)

Interfaces in Go (Part-2)

ยท

9 min read

Type assertion

Type assertion is used to get the underlying concrete value of the interface variable. i.(Type) this is the syntax of type assertion, where:

i -> interface variable

Type -> type that implements the interface

Let's see an example of type assertion.

package main

import "fmt"

type Person interface {
    info() string
}

type Student struct {
    name string
}

func (s Student) info() string {
    return fmt.Sprintf("Student name is %s\n", s.name)
}

func main() {
    s := Student{
        name: "Barry Allen",
    }
    var p Person = s
    name := p.(Student)
    fmt.Println(name)
}
{Barry Allen}

Run this code in Go Playground

In the above example, we create a Person interface which is then implicitly implemented by the Student struct by implementing the info method. var p Person = s this statement creates an interface variable p and assigns Student struct type variable s to it. name := p.(Student) this statement extracts the concrete value of the type Student and assign it to the name variable.

In the above program, type Student implements the Person interface and hence p.(Student) can hold the concrete value of type Student. But if Type from i.(Type) does not implement the interface then Go will throw a compilation error. Below is an illustrative example.

package main

import "fmt"

type Person interface {
    info() string
}

type Student struct {
    name string
}

func main() {
    var p Person
    name := p.(Student)
    fmt.Println(name)
}
./prog.go:15:11: impossible type assertion:
    Student does not implement Person (missing info method)

Run this code in Go Playground

If the Type implements the interface but if the concrete value of that type is not available then Go will panic in runtime.

package main

import "fmt"

type Person interface {
    info() string
}

type Student struct {
    name string
}

func (s Student) info() string {
    return fmt.Sprintf("Student name is %s\n", s.name)
}

func main() {
    var p Person
    name := p.(Student)
    fmt.Println(name)
}
panic: interface conversion: main.Person is nil, not main.Student

goroutine 1 [running]:
main.main()
    /tmp/sandbox657831697/prog.go:19 +0x45

Run this code in Go Playground

Similarly, if the concrete type of interface variable does not match with Type of i.(Type) then the program will panic in runtime.

package main

import "fmt"

type Person interface {
    info() string
}

type Student struct {
    name string
}

func (s Student) info() string {
    return fmt.Sprintf("Student name is %s\n", s.name)
}

type Teacher struct {
    name string
}

func (s Teacher) info() string {
    return fmt.Sprintf("Teacher name is %s\n", s.name)
}

func main() {
    t := Teacher{
        name: "Richard Feynman",
    }
    var p Person = t
    name := p.(Student)
    fmt.Println(name)
}
panic: interface conversion: main.Person is main.Teacher, not main.Student

goroutine 1 [running]:
main.main()
    /tmp/sandbox133075599/prog.go:30 +0x45

Run this code in Go Playground

To avoid the above panic errors Go has another way for type assertion.

value, ok := i.(Type)

In the above syntax, Ok is a boolean variable. It will be false if i.(Type) has no concrete value and vice versa.

package main

import "fmt"

type Person interface {
    info() string
}

type Student struct {
    name string
}

func (s Student) info() string {
    return fmt.Sprintf("Student name is %s\n", s.name)
}

type Teacher struct {
    name string
}

func (s Teacher) info() string {
    return fmt.Sprintf("Teacher name is %s\n", s.name)
}

func main() {
    var p Person
    name, ok := p.(Student)
    fmt.Println(name, ok)

    t := Teacher{
        name: "Richard Feynman",
    }
    var p1 Person = t
    name1, ok1 := p1.(Teacher)
    fmt.Println(name1, ok1)

    name2, ok2 := p1.(Student)
    fmt.Println(name2, ok2)
}
{} false
{Richard Feynman} true
{} false

Run this code in Go Playground

Type switch

Type switch is similar to switch case statements where type switch compares the concrete type of an interface against types specified in cases. Below is the syntax of the type switch:

switch i.(type) {
    case float32:
        // Something to do
    case int:
        // Logic
    default:
        // Default logic
    }

Here i.(type) the syntax is fairly similar to type assertion syntax, where i is interface but instead of value type we use keyword type.

package main

import (
    "fmt"
)

func guessType(i interface{}) {
    switch i.(type) {
    case string:
        fmt.Println("It is string")
    case int:
        fmt.Println("It is Int")
    case bool:
        fmt.Println("It is boolean")
    default:
        fmt.Println("IDK")
    }
}

func main() {
    guessType(10)
    guessType("Hello")
    guessType(true)
    guessType(10.11)
}
It is Int
It is string
It is boolean
IDK

Run this code in Go Playground

In the above example, we're passing an empty interface as an argument to function. The empty interface is implemented by all the types so we can pass any type of parameter while calling that function.

i.(type) evaluates the concrete type of an interface and then a matching case is executed if none of the cases is matched then the default case is executed.

Implementing interfaces using pointer receivers vs value receivers

In Golang methods can have both pointer or value as a receiver which restricts the accessibility of methods only to the type of its receiver. Methods from an interface can be pointers or values as a receiver in their implementation.

Implementing interface with value receiver

package main

import (
    "fmt"
)

type Car interface {
    information()
}

type Ferrari struct {
    name   string
    colour string
}

func (f Ferrari) information() {
    fmt.Printf("Name of the car: %s\n", f.name)
    fmt.Printf("Colour of the car: %s\n", f.colour)
    fmt.Println()
}

func main() {
    var c1 Car

    f := Ferrari{
        name:   "SF90 STRADALE",
        colour: "ROSSO CORSA",
    }
    c1 = f
    c1.information()

}
Name of the car: SF90 STRADALE
Colour of the car: ROSSO CORSA

Run this code in Go Playground

In the above code, type Ferrari is implementing the method information from an interface Car with a value receiver and in the main function, we're assigning the variable f of struct Ferrari to variable c1 of an interface Car and then calling information method using interface variables.

Implementing interface with pointer receiver

package main

import (
    "fmt"
)

type Car interface {
    information()
}
type Porsche struct {
    name   string
    colour string
}

func (p *Porsche) information() {
    fmt.Printf("Name of the car: %s\n", p.name)
    fmt.Printf("Colour of the car: %s\n", p.colour)
    fmt.Println()
}

func main() {

    p := &Porsche{
        name:   "718 SPYDER",
        colour: "BLACK",
    }
    var c2 Car = p
    c2.information()
}
Name of the car: 718 SPYDER
Colour of the car: BLACK

Run this code in Go Playground

In the above code, type Porsche is implementing the method information from an interface Car with a pointer receiver and in the main function we're assigning a variable pointer p of struct Porsche to variable c2 of an interface Car and then calling information method using interface variables.

In Golang, a method with a value receiver or pointer receiver can be called on either value or pointer. It means if you're implementing a method with a value receiver then you can use a value or pointer of that type to call that method.

But in the case of an interface does this phenomenon work?

Calling a method with value receiver on pointer type

In the below code, we implement the method information with value receiver and call it on pointer type.

package main

import (
    "fmt"
)

type Car interface {
    information()
}

type Ferrari struct {
    name   string
    colour string
}

func (f Ferrari) information() {
    fmt.Printf("Name of the car: %s\n", f.name)
    fmt.Printf("Colour of the car: %s\n", f.colour)
    fmt.Println()
}

func main() {
    var c Car
    f1 := &Ferrari{
        name:   "SF90 STRADALE",
        colour: "ROSSO CORSA",
    }
    c = f1
    c.information()
}
Name of the car: SF90 STRADALE
Colour of the car: ROSSO CORSA

Run this code in Go Playground

In the above code, the pointer to value conversion is done by Golang implicitly. It is valid to call a method with value type on any value or anything whose value can be dereferenced.

Calling a method with a pointer receiver on the value type

In the below code, we implement the method information with a pointer receiver and call it on the value type.

package main

import (
    "fmt"
)

type Car interface {
    information()
}

type Porsche struct {
    name   string
    colour string
}

func (p *Porsche) information() {
    fmt.Printf("Name of the car: %s\n", p.name)
    fmt.Printf("Colour of the car: %s\n", p.colour)
    fmt.Println()
}

func main() {
    var c Car

    p := Porsche{
        name:   "718 SPYDER",
        colour: "BLACK",
    }
    c = p
    c.information()

}
./prog.go:29:4: cannot use p (type Porsche) as type Car in assignment:
    Porsche does not implement Car (information method has pointer receiver)

Run this code in Go Playground

In the above code, we're implementing the information method using a pointer receiver and then calling it on value type instead of a pointer, and we're getting a compilation error. So what's happening?

If we look at the implementation of the information method it has a pointer receiver and we're calling it on p which is a value type and does not implement the Car interface. Hence we see the compilation error.

But in Golang, the method with value receiver or pointer receiver can be called on either value or pointer then why is this failing here?

This is because: it is completely valid to call the pointer receiver method using a pointer or using any value whose address can be obtained. In this case, the value type assigned to the interface variable is a concrete value of the interface whose address can not be obtained and hence we get that compilation error.

Note: If we call a method directly with a struct variable then this issue will not occur.

Comparing the interface's variables

The two interface variables are comparable only if the concrete value and concrete type are comparable. Interface variables can be compared with == and != operators.

If the concrete types or concrete values are not the same then the interface variables are not equal. If the concrete types or concrete values are the same then the interface variables are equal. Variables of an empty interface are always equal. Read more on Golang comparison operators.

package main

import "fmt"

type Int int

func (i Int) String() string {
    return fmt.Sprintf("Value of Int is %d", i)
}

type Float float32

func (f Float) String() string {
    return fmt.Sprintf("Value of Float is %f", f)
}

func main() {
    var i Int = 10
    var f Float = 5.5

    var v1 fmt.Stringer = i
    var v2 fmt.Stringer = f

    fmt.Println(v1 == v2) // false, because concrete types are not same

    var i1 Int = 6
    var v3 fmt.Stringer = i1

    fmt.Println(v1 == v3) // false, because concrete values are not same

    var f2 Float = 5.5
    var v4 fmt.Stringer = f2

    fmt.Println(v2 == v4) // true

    var m, n interface{}
    fmt.Println(m == n) // true
}

Run this code in Go Playground

Some good resources to read

Go Data Structures: Interfaces by Russ Cox

The Laws of Reflection by Rob Pike

Thank you for reading this blog, and please give your feedback in the comment section below.

Did you find this article valuable?

Support Pratik Jagrut by becoming a sponsor. Any amount is appreciated!

ย