Interfaces in Go

Posted:
10/06/2023
| By:
Jaydevsinh Chauhan

Object-oriented programming (OOP) is a programming paradigm that organizes code based on the concept of objects. In Go, a programming language created by Google, OOP is applied somewhat differently than traditional OOP languages, such as Java or C++. While Go does not have classes or traditional inheritance, it provides a unique approach to structuring code using composition and interfaces. In this blog, we’ll explore in greater detail some of the most critical concepts of OOP in Go.

Structure in Go

Structure is a composite data type that allows us to define the collection of fields together as a single entity. It’s also commonly referred to as a struct.

Struct is used to represent real-world objects or concepts by grouping related data together. Struct is used mainly when we need to define a schema made of different individual fields. Since we can instantiate structure, there should be some nomenclature distinction between structure and instance in Go.

Because of this, the name struct type is used to represent structure schema, and the word struct or structure alone is used to represent instance. In other words, “struct type” is schema containing the blueprint of the data structure. To make things simple, we create a new derived type so that we can refer to the struct type easily. We use the struct keyword to create a new structure type, which you can see below.

type StructName struct{
       field1 fieldType1
       field2 fieldType2
}

In this example, StructName is struct type, while fieldType1 and fieldType2 are fields of data type. We can also define different fields of the same data type in the same line, which can be seen below.

type Employee struct{
      firstName,lastName string
      salary              int
      fullTime            bool
}

When we say struct, we refer to the variables that hold the value of employee data types. A zero value of a struct is a struct with all fields set to their own zero values. Consider var ross employees—here, the employee is a struct type, while ross is a struct keyword that’s a built-in type.

Getting and setting struct fields

When struct variables are created, we can access their fields using . (dot) operator. To assign the value to the firstName field, we use syntax Rahul. So, firstName equals “Rahul.” Rather than creating an empty struct and assigning values to its fields individually, we can create a struct with field values initialized in the same syntax. You can see an example of this below.

Rahul:= Employee{
       firstName:"Rahul",
       lastName:"Sharma",
       fullTime:true,
       salary:1200,
}

We can use shorthand notation, such as using : to equal syntax, to create variable Rahul so that Go can infer the employee type automatically. The order of appearance of a struct’s fields doesn’t matter, but it’s important to note that a comma (,) is always necessary after the value assignment of the last field while initializing the struct. This way, Go won’t add a semicolon just after the last field while compiling code.

We can also initialize only some fields of the struct and leave others to their zero values. Another way of initializing the struct that doesn’t include field name declarations is:

Rahul:=Employee{"Rahul","Sharma",1200,true}

The above syntax is perfectly valid, but when creating a struct without declaring field names, we must provide all field values in order of their appearance in struct type.

Anonymous structs

Anonymous structs are structs with no explicitly defined derived struct type. In the case of an anonymous struct, we do not define any derived struct type. We create a struct by defining the inline or anonymous struct type, and initial values of the struct fields in the same syntax. See an example below.

Monica:=struct{
       firstName,lastName string
       salary int
       fullTime bool
}{
       firstName:"Monica",
       lastName:"Sharma",
       salary:1200,
}

In this case, we’re creating the struct “Monica” without defining the derived struct type. This can be useful for when we don’t want to re-use struct types. Creating a derived type from built-in struct types gives us the flexibility to reuse it without having to write complex syntax again and again.

Pointer to struct 

The syntax to create a pointer to struct is:

Rahul:= &Employee{
      firstName:"Rahul",
      lastName:"Sharma",
      fullTime:true,
}

Since Rahul is the pointer, we need to use *Rahul dereferencing syntax to get the actual value of the struct it’s pointing to, and use (*Rahul).firstName to access firstName of that struct value. We’re using parenthesis around the pointer dereferencing syntax so that the compiler doesn’t get confused between (*Rahul).firstName and (*Rahul.firstName).

It’s important to note that Go provides easier alternative syntax to access fields. We can access fields of the struct pointer without dereferencing it first. Go takes care of dereferencing the pointer under the hood.

Rahul:=&Employee{
      firstName:"Rahul",
      lastName:"Sharma",
      salary:1200,
      fullTime:true,
}
fmt.Println("firstName",Rahul.firstName)//Rahul is a pointer

Method

Method is a function that’s defined with a different syntax than a normal function. It requires additional parameters, known as receivers, which are types to which functions belong. This way, the method (or function), can access properties of the receiver it belongs to, such as fields of a struct. To convert function to method, we must add the receiver parameter in the function definition. See an example of this below:

func(r Type)functionName(params)returnValues{
       ...
}
type Employee struct{
      firstName, lastName string
}
func(e Employee)fullName()string{
      return e.firstName+" "+e.lastName
}
e:= Employee{
      firstName:"Rahul",
      lastName:"Sharma",
}
fmt.Println(e.fullName)

In this case, the FullName() method will belong to any object of type Employee. This means that the object will automatically get this method as a property. When this method is called on, it will receive the object as receiver e. The receiver of the method is accessible inside the method body—we can access e inside of the method body of fullName().

Since the receiver is the struct of type Employee, we can access any fields of the struct. As the method belongs to the receiver type and it becomes available on that type as a property, we call that method using Type.methodName(params) syntax. For methods, we don’t have to provide properties of struct because the method already knows about them.

One major difference between functions and methods is that we can have multiple methods with the same name, while no two functions with the same name can be defined in the package. We’re allowed to create methods with the same names, so long as their receivers are different.

Pointer receivers

type Employee struct{
      name string
}
func (e *Employee)changeName(newName string){
      (*e).name=newName
}
e:= Employee{
      name:"Rahul",
}
ep=&e
ep.changeName("Monica")

When the method belongs to the type, its receiver receivers a copy of the object on which it was called. This means any changes made to the copy inside the method did not affect the original struct. However, the method can also belong to the pointer of type.

func (r *Type)functionName(params)returnValues{
        ...
}

When the method belongs to the pointer of type, its receiver will receive pointer to the object instead of the object copy. We solely created the pointer ep from e just to call the method changeName on it, but we can also use (&e).changeName(“Monica”) syntax instead of creating a new pointer.

Calling methods with pointer receivers on values

If the method has a pointer receiver, then we don’t necessarily need to use the pointer dereferencing syntax (*e) to get the value of the receiver. We can use a simple e, which will be the address of the value that the pointer points to. Go will understand that we’re trying to perform an operation on the value itself, so under the hood it will convert e to (*e).

We also don’t necessarily need to call the method on the pointer if the method has a pointer receiver. We’re allowed to call this method on the value instead, because Go will pass the pointer of the value as the receiver. We can decide between the methods with pointer receivers or value receivers, depending on the use case. However, methods with pointer receivers are preferred, as no new memory is created for operations.

Methods can accept both pointer and value. When the normal function has the parameter definition, it will only accept the argument of types defined by that parameter. Go converts the type behind the scenes.

Methods can also receive any type, as long as the type definition and method definition are in the same package. If we want to define the methods on type, and not defined in the main package, we need to create a new derived type from it. This way, both the method and the newly defined type will belong to the same package.

Interface

Interface is a collection of method signatures that the Type can implement using said methods. The interface defines—not declares—the behavior of an object. This means that not only the struct but even the derived type in Go can implement the methods, which are defined by the interface. The primary job of interface is to provide only the method signatures consisting of method name, input arguments, and return types. It’s up to the Type, such as struct type, to declare the methods and implement them. For example:

Stack can perform pop and push operations. If the interface defines method signatures for pop and push while stack implements them, stack is said to implement that interface.

In Go, you can declare interface using the type keyword, followed by the interface name and set of method signatures.

type MyInterface interface{
       Method1()returnType1
       Method2(param1 type1,param2 type2)returnType2
}

In the example above, we’ve defined MyInterface interface, which has two methods: Method1 and Method2. Any type that implements these methods with their exact method signatures will also implement MyInterface interface. Since the interface is the type, just like the struct we can create a variable of its type. In the above case, we can create the variable ml of the type interface MyInterface.

Interface has two types. The static type of interface is interface itself. Interface doesn’t have static value—rather, it points to dynamic value. The variable of interface types can hold the value of the type that implements interface, which makes the value of that type dynamic, making it become the dynamic type of interface. See an example below of how to write a program which implements interface Shape.

type Shape interface{
       Area()float64
       Perimeter()float64
}
type Rect struct{
       width,height float64
}
func (r Rect)Area()float64{
       return r.width*r.height
}
func (r Rect)Perimeter()float64{
       return 2*(r.width+r.height)
}
type Circle struct{
       radius float64
}
func (c Circle)Area()float64{
       return 3.14*c.radius*c.radius
}
func (c Circle)Perimeter()float64{
       return 2*3.14*c.radius
}
func PrintShapeDetails(s Shape){
      fmt.Printf("Area:%.2f\n",s.Area())
      fmt.Printf("Perimeter:%.2f\n",s.Perimeter())
}
func main(){
      rectangle:=Rect{width:5,height:3}
      circle:=Circle{radius:2.5}
      PrintShapeDetails(rectangle)
      PrintShapeDetails(circle)
}

In the above program, we’ve created the Shape interface and struct type Rect and Circle. Then, we defined the methods like Area and Perimeter, which belong to Rect type. Therefore, Rect implemented those methods.

Since these methods are defined by the Shape interface, both struct types implement the Shape interface. We haven’t forced them to implement the Shape interface, meaning it’s all happening automatically. This is what we mean when we say that interfaces in Go are implicitly implemented. When the type implements the interface, the variable of that type can also be represented as the type of interface. We can confirm that by creating nil interface 's' of type Shape and assigning the struct of type Rect. You can also see this in above example—see function PrintShapeDetails.

Since both Rect and Circle implement the Shape interface, we’ve achieved polymorphism. From the example, we see that the dynamic type of “s” is now Rect and the dynamic value of “s” is the value of the struct, Rect, which is {5 3}. We call this dynamic because we can assign “s” with new structs of different struct types which also implement the interface Shape.

Sometimes, the dynamic type of interface is also called the concrete type, because when we access this type of interface it returns the type of its underlying dynamic value, and its static type remains hidden. If, for the Rect struct, we don’t implement one of the methods, we get an error. So in order to successfully implement the interface, you must implement all of the methods declared by that interface with the exact signatures.

Empty interface

An empty interface has zero methods and is represented by interface {}. Since the empty interface has no methods, all types implement this interface implicitly. See below for an example that demonstrates the usage of an empty interface in Go.

func PrintDetails(value interface{}){
       fmt.Println("Value:", value)
}
func main(){
// Passing different types to PrintDetails function
       PrintDetails(42)
       PrintDetails("Hello,World!")
       PrintDetails(true)
       PrintDetails(3.14)
}

In this example, we define the function PrintDetails, which takes the parameters of type interface{}. This means it can accept values of any type.

Multiple interface

Let’s modify the Shape interface example to include an additional interface called Resizable. Resizable interface will define the method Resize (factor float64) to resize the shape.

type Shape interface{
       Area()float64
       Perimeter()float64
}
type Resizable interface{
       Resize(factor float64)
}
type Rect struct{
       width,height float64
}
func (r Rect)Area()float64{
       return r.width*r.height
}
func (r Rect)Perimeter()float64{
       return 2*(r.width+r.height)
}
func (r *Rect)Resize(factor float64){
       r.width*=factor
       r.height*=factor
}
func main(){
       rectangle:=&Rect{width:5,height:3}
       var resizable Resizable=rectangle
       fmt.Printf("Area:%.2f\n",shape.Area())
       fmt.Printf("Perimeter:%.2f\n",shape.Perimeter())
       resizable.Resize(1.5)
       fmt.Printf("Updated Area:%.2f\n",shape.Area())
       fmt.Printf("Updated Perimeter:%.2f\n",shape.Perimeter())
}

In this example, the Rect struct implements both the Shape and Resizable interfaces. It provides implementations for Area(), Perimeter(), and Resize(factor float64) methods. By implementing both the Shape and Resizable interfaces, the Rect struct can be treated as both a shape and resizable object, enabling polymorphic behavior and allowing us to work with it through the different interfaces.

But what if we try to call the Resize method on shape var? Well, we get an error. This program will not compile because the static type of shape is Shape and the static type of resizable is Resizable. Since Shape does not define the Resize method and Resizable does not define the Area and Perimeter methods, we get an error.

To make it work we need to somehow extract the dynamic value of these interfaces, which is a struct of type Rect, and Rect implements these methods. This can be done using type assertion.

Type assertion

We can find out the underlying dynamic value of the interface using the syntax “i”.(Type) where “i” is the variable of the type interface and Type is the type that implements the interface. Go will check if the dynamic type of “I” is identical to the Type, and return dynamic value if possible. Here’s how the updated main method would look with type assertion.

func main(){
      rectangle:=&Rect{width:5,height:3}
      var shape Shape=rectangle
      polygon:=shape.(Rect)
      fmt.Printf("Area:%.2f\n",polygon.Area())
      fmt.Printf("Perimeter:%.2f\n",polygon.Perimeter())
      polygon.Resize(1.5)
      fmt.Printf("Updated Area:%.2f\n",polygon.Area())
      fmt.Printf("Updated Perimeter:%.2f\n",polygon.Perimeter())
}

It’s important to be aware that in type assertion syntax i.(Type), if “i” cannot hold the dynamic value of the type Type, it’s because Type doesn’t implement the interface, and Go will throw a compilation error. However, if Type implements the interface but “i” does not have a concrete value of Type, then Go will panic in runtime.

To avoid runtime panic, there’s another variant of type assertion syntax that will fail silently:

value,ok:= i.(Type)

In the above syntax, we can check using the ok variable if “i” has a concrete type Type or dynamic value of Type. If it doesn’t, then ok will be false and the value will be zero value Type. How would we know if the underlying value of the interface implements any other interfaces? This is also possible using type assertion. If the Type in type assertion syntax is interface, then Go will check if the dynamic type of “i” implements the interface Type

Interface comparison

Two interfaces can be compared with == and != operators. Two interfaces are always equal if underlying dynamic values are nil, which means two nil interfaces are always equal. Hence '==' operation returns true. For example:

var a,b interface{}
fmt.Println(a==b)//true

If these interfaces are not nil, then their dynamic types (type of their concrete values) should be the same, and the concrete values should be equal. If the dynamic types of interfaces are not comparable—for example, slice, map, and function or if the concrete value of interface is a complex data structure like slice or array that contains these incomparable values—then == or != operations will result in runtime panic. If one interface is nil, then the == operation will always return false.

Interface in Go

In conclusion, Go’s approach to OOP prioritizes practicality, readability, and maintainability. It may feel different from the other languages’ OOP paradigms, but it enables efficient and effective coding practices that result in reliable software that’s easy to work with.