Interface

Go has a special data type called interface. It is meant to fill in the gap between data structure and control path that most programming (especially C) encounters.

Using Interface

Interface is usable in various ways.

As a Function Offered from Data Structure

One good way of using interface is to make data structure (struct) to offer functions, similar (but not the same) to class in object-oriented programming. Hence, this greatly reduces the overloading and complexities of the Go package's functions by grouping the necessary functions to the corresponding data structure. Here is an example:

package main

import (
        "fmt"
)

// A is a data structure.
type A struct {
}

// Print is a function to to print the name out.
func (o *A) Print(name string) {
        fmt.Printf("Struct A: %v\n", name)
}

func main() {
        x := &A{}
        x.Print("Jane")
}

// Output:
// Struct A: Jane

Notice that A structure can quickly perform a function by calling its associated function (x.Print("Jane")) instead of data structure passing (func Print(a, "Jane"))? This helps in reducing cyclic dependencies by having the closely related function tied together with the data structure.

Note that Go treats interfacing function public and private depending on the first character of the name. If it is Capitalized (Print), it is a public function and you need to document it by commenting right above it (e.g. // Print is a function to to print the name out.). Otherwise, if it is lowercase (print), it is private to that package consumption.

Outside of package, any client can only calls the public interfacing function.



As a Functional Interface

The interface allows the unification of interfaces under a single patterns. This provides freedom in data structure (struct) design where developer is not limited to locking one structure down. Also, it permits controlled duck-typing.

As long as the structure offers the interface function matches the interface declaration, it is permit-table. Here is an example:

package main

import (
        "fmt"
)

type printer interface {
        execute(name string)
}

type a struct {
}

func (o *a) execute(name string) {
        fmt.Printf("a: %v\n", name)
}

type b struct {
}

func (o *b) execute(name string) {
        fmt.Printf("b: %v\n", name)
}

// printer function
func printOut(p printer, name string) {
        if p == nil {
                return
        }
        p.execute(name)
}

func main() {
        x := &a{}
        y := &b{}
        printOut(x, "Jane")
        printOut(y, "Sammy")
}

// Output:
// a: Jane
// b: Sammy

Notice that we have a printer interface that declares unification method like execute. Then in printOut function, it is used as a pass-in variable interface instead of specific struct. This allows different structure (a and b) that offers execute function to be usable as parameter for printOut.

We can also vividly see that printOut abstracts / encapsulate / isolate a high-level execution codes from low level printer execution codes.

There is a caveat however, if the object is using pointer interface (func (o *b) execute(name string) {), you need to make sure you create a pointer object (y := &b{}) and before passing into printOut. Otherwise, interface will not recognize the structure having the method and throws you a compile error.



As an Omni-Type Parameter

You can also use interface as an onmi-type parameter for a function. This allows any client to fill in any type of variable at runtime regardless of type checking. Here is an example upgraded from the above:

package main

import (
        "fmt"
)

// A is a data structure.
type A struct {
}

// Print is a function to to print the name out.
func (o *A) Print(data interface{}) {
        name, ok := data.(string)
        if !ok {
                return
        }
        fmt.Printf("Struct A: %v\n", name)
}

func main() {
        x := &A{}
        x.Print("Jane")
        x.Print(124.1241)
}

// Output:
// Struct A: Jane

Notice that we upgraded the Print function to accept interface type parameter (data interface{}). This allows Print to accept various data types (string: x.Print("Jane"), float64: x.Print(124.1241)). When using such pattern, the user must always assert the the omni-type data variable back to its original type before use. Otherwise, you will get a compilation error / runtime panic depending on severity.

Asserting Type Back From Interface

If you're using interface as an object parameter or variable type, you need to perform type assertion to that variable before using it. There are 2 ways of doing it. It is always the best practice to identify data type before use. Otherwise, due to the freedom offered by interface type, unless the control path is type guaranteed, you're easily prone to data type panics for crashes.

Single (Assured) Type

If you're very sure about a single data type you want to use (assured with confidence), then you can use the following if mechanism:

original, ok := entity.(Type)
if !ok {
        // do something about missing data you're looking for
}
operate := with + original


Multiple Types

If you're expecting more than one type (e.g. pointer or data), you should use switch mechanism:

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)
        // handle error
}

Checking Nil Value

Interface is one of the occurrence where nil value does not directly means nil. A direct nil comparison to an interface only checks for its nil interface, not the underlying nil value. Hence, we have 2 types of nil values checking:


Check for Nil Interface

If the interface is nil (interface that does not offer any functions), you can perform a direct comparison like all other variable types. Example: a == nil


Check for Underlying Nil Value

At minimum, you need to use the switch type mechanism to root out all types and organize bool result to match its existence value. Here is an example:

func interfaceHasNilValue(data interface{}) bool {
        exist := false
        switch x := data.(type)  {
        case bool:
                if x {
                        exist = true
                }
        default:
                if x != nil {
                        exist = true
                }
        }
        return exist
}

Of course, this is the generalized nil value checking. You can add in more custom cases that matches your needs.

That's all about Go Interface type.