Sitemap
·

Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Interfaces in Golang: A Deeper Look

7 min read
·
Apr 14, 2024

--

Interfaces in Golang are fairly different from other languages. In Go, the interface is a custom type that is used to specify a set of one or more method signatures.

A value of interface type can hold any value that implements those methods. The interface is abstract, meaning we can’t create instances of an interface. What we do is create instances of concrete values that implement that interface.

A very popular example is the Stringer interface in Golang:

type Stringer interface {
String() string
}

Now, any types we create that have a String method which returns a string will satisfy the Stringer interface.

type StringerStruct struct {

}

func (ss StringerStruct) String() string { return "stringer_struct" }

Interfaces play a significant role in achieving polymorphism and abstraction in Golang. They decouple the definition of objects from their implementation, promoting flexibility and reusability in code. This article however, won’t talk about how these are achieved using interfaces. There’s many other places where you can read about it.

Interfaces, Static or Dynamic?

One, Go’s static type system ensures that the types involved in interface implementation are compatible, at compile time. This means that the compiler checks if the methods required by an interface are present in the concrete type.

However, the actual assignment between the interface variable and the value of the concrete type happens at runtime. This allows for flexibility in choosing the appropriate implementation of an interface dynamically during program execution.

Other languages:

For languages like Java and C++, there are method call tables and virtual method tables (vtables). These tables are used to implement dynamic method dispatch, which allows the correct method to be called for an object at runtime based on its actual type.

Java

In Java, method resolution in the context of interfaces is achieved through a mechanism called dynamic dispatch or runtime polymorphism.

When a method is called on an object through an interface reference, the JVM dynamically resolves the method invocation at runtime. It first determines the actual type of the object being referred to (known as the runtime type). Then, it looks for the appropriate method implementation in the class hierarchy of the runtime type.

If the method is not directly implemented by the runtime type, Java looks for it in the superclass chain.

Press enter or click to view image in full size

C++

In C++ runtime polymorphism is achieved using virtual functions. When a class contains at least one virtual function, the compiler creates a vtable for that class. The vtable is an array of function pointers, where each entry corresponds to a virtual function declared in the class. Each object of a class with virtual functions contains a hidden pointer called the vptr, which points to the vtable associated with the class.

When a virtual function is called through a base class pointer or reference, the compiler resolves the function call dynamically at runtime. It uses the vptr associated with the object to access the vtable. The vptr helps in identifying the correct vtable for the actual derived class type of the object.

Once the correct vtable is identified, the compiler uses the index of the virtual function to retrieve the corresponding function pointer from the vtable. Finally, it invokes the function through the function pointer.

Press enter or click to view image in full size

Finally, Golang

Now, let’s consider an example of how interfaces and structs are typically used.

type Vehicle interface {
Accelerate()
Brake()
}

type Car struct {
name string
color string
age int
}

func (c Car) Accelerate() {}
func (c Car) Brake() {}
func (c Car) Airbag() {}

func main() {
var v Vehicle
c := Car{"Mazda", "Red", 5}
v = c
}

Go compiler has type description structures. Check the Type at https://github.com/golang/go/blob/master/src/internal/abi/type.go.

For every single type created in Golang and for functions, a type description structure is created. These are InterfaceType for interfaces, StructType for structs, and so on. These have Type embedded in them to reuse metadata common to all type descriptions. Check the type.go file linked above.

An interface is represented using two “words” in memory.

A word is a collection of bits that can be transferred to the CPU as a single unit. For 32-bit systems the word is of length 32 bits, for 64-bit systems the length is, 64.

Let’s assume we’re working with a 64-bit architecture. The interface thus, use two words, each 64 bits, and uses these words to store the pointer of 2 things.

  • The interface table(called itab)
  • The copy of the concrete struct the interface has the value of(called data)

Hence, when we assigned v as the Vehicle we initialized both of these values.

Here’s how the interface looks like in code:

type iface struct {
tab *itab
data unsafe.Pointer
}

itab points to the interface table and data points to the copy of the struct.

Press enter or click to view image in full size

Let’s focus specifially on the first point, the itab. Here’s what itab looks like:

type ITab struct {
Inter *InterfaceType
Type *Type
Hash uint32 // copy of Type.Hash. Used for type switches.
Fun [1]uintptr // variable sized. fun[0]==0 means Type does not implement Inter.
}

Notice how it has a Type and InterfaceType which is the same type description structure we were talking about.

type InterfaceType struct {
Type
PkgPath Name // import path
Methods []Imethod // sorted by hash
}

type Imethod struct {
Name NameOff // name of method
Typ TypeOff // .(*FuncType) underneath
}

The InterfaceType has the Methods which is all the methods the interface has.

Notice that in the ITab, along with the InterfaceType there’s the last field Fun. This is the function pointer. This stores the methods of the concrete type which implement the interface. These will only be the methods that the interface has, meaning methods of the Car struct that are not in Vehicle will not be included here, like Airbag(). The Fun array is declared with a fixed size to optimize memory usage and improve performance. Since most interfaces have a small number of methods, allocating space for only a few method pointers in the Fun array reduces memory overhead. However, it’s conceptually variable-sized because it can hold multiple method pointers corresponding to the methods defined by the interface. This allows interfaces with multiple methods to be efficiently represented in memory.

Press enter or click to view image in full size

Keep in mind that the type description structures for each interface and struct are generated all at compile time. These are loaded into memory in a global table by the Go compiler.

The interface table is not created at compile time. Notice how a seperate itable is created for each interface-struct pair, hence it might be inefficient to create ITab for each pair at compile time, alot of them won’t be needed!

When the ITab is created for a specific interface-struct pair, it is computed once and than cached for future use. This prevents redundancy and duplicate computation of the same itables for the same pairs, and also makes access to methods of the interface quick.

One might think that this might make things slow, which is true. If a concrete type has n methods and the interface has m, than to compute all itables, it might take O(n*m) time. However, observe that the Methods []Imethod in ITab was sorted by the hash of the method, which makes the computation O(n+m), this is an optimization done by the folks who maintain Go.

When we do a type switch, the go compiler generates code which accesses the type of the interface and checks if it matches the type of the concrete value at runtime. Similarly, at points where the method of an interface is accessed like this:

func accelerateCar(v Vehicle) {
v.Accelerate()
}

accelerateCar(c)

The statement v.Accelerate, will also be replaced by the method in the method list in Fun.

Optimizations

There’s some more optimizations done for the itab. If an empty interface{} or any is used, than it has no methods, and creating a full itab would be inefficient, hence, in that case instead of the itab, the type is directly pointed to.

var v any = Car{"Mazda", "Red", 5"}
Press enter or click to view image in full size

There’s a seperate struct for this in Golang’s runtime eface.

type eface struct {
_type *_type
data unsafe.Pointer
}

Also, if the concrete value is small in size, that is if it fits in a single word than the data in iface isn’t a pointer to the data but instead holds the entire data directly. Example if the concrete struct has size less than 64 bits than data holds the entire value directly. This is also managed by the go runtime and not something we have to manager on our own.

Press enter or click to view image in full size

That’s it for now.

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--

Stackademic
Stackademic

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Responses (1)

Morty Proxy This is a proxified and sanitized view of the page, visit original site.