Interface is a definition, or an abstract type, which defines the expected behavior from an object (type). It is just a named collection of method signatures and doesn’t have its own implementation, but it is said to be implemented if any other types implement all the methods defined in it. So, it is used when you need to decouple objects/types but has some common usage
Let's look at an example:
Here we have a “Laptop” struct. And we’ve declared PriceWithVAT and Category function as pointer receiver function that receives the pointer to the Laptop struct.
type Laptop struct {
Id string
Brand string
Series string
Price float64
}
func (l *Laptop) PriceWithVAT() float64 {
return l.Price * 13
}
func (l *Laptop) Category() string {
return "COMPUTER"
}
Similarly, Here we have a “Bike” struct. And we’ve declared PriceWithVAT and Category function as pointer receiver function that receives the pointer to the Bike struct.
type Bike struct {
Id string
Name string
Power float32
Price float64
}
func (b *Bike) PriceWithVAT() float64 {
return b.Price * 24
}
func (b *Bike) Category() string {
return "VEHICLE"
}
Now let's assume we need a Sell function for both products (bike and laptop). We could achieve this by creating two different functions like this:
func SellBike(b Bike){
// sell bike functions here
}
func SellLaptop(l Laptop){
//sell laptop functions here
}
But since both functions have the same features.. we could instead create an interface defining both the methods.
type Product interface {
PriceWithVAT() float64
Category() string
}
Here We have a product interface that defines “PriceWithVAT” and “Category” methods. Since both the struct (Bike and Laptop) have implemented the method, the Product interface is implicitly implemented. You don’t need to explicitly use the keyword “implement” in golang.
Now, If a function has the Product interface as an argument, all structs can be passed down to the function as the argument if it satisfies the interfaces regardless of what those structs do and what they are meant for. So, this is how we achieve Polymorphism in golang.
func Sell(product Product) {
// do some things here
}
Then, We can use this function like this:
func main() {
b := Bike{Id: "1234", Name: "Pulsar", Power: 190, Price: 1200}
l := Laptop{Id: "1234", Brand: "Dell", Series: "Pavillion", Price: 900}
Sell(&b)
Sell(&l)
}
The Sell function is accepting a Product interface. Since both Bike and Laptop types implement the Product interface, we can pass both of them to the Sell function without any type error. It doesn’t really matter what these two structs Bike and Laptop do as long as they have methods called PriceWithVAT returning string and Category returning a string.
Using Empty Interface as a function argument
It's also common practice in Golang to use an empty interface as an argument. This is often used when you need the function to accept any data type. It is possible as the empty interface has no methods, no rules, and hence by default it is implemented by all types.
For example, if we replaced the above Sell function with an empty interface it would work just fine and can accept any datatypes as well
func Sell(data interface{}){
// Sell functions here
}
func main() {
b := Bike{Id: "1234", Name: "Pulsar", Power: 190, Price: 1200}
l := Laptop{Id: "1234", Brand: "Dell", Series: "Pavillion", Price: 900}
Sell(&b)
Sell(&l)
Sell("string") // Can accept any data type
}
Using Empty interface as a data type
Since an empty interface is like a wildcard and can be used in place of every other data types, we can also assign any data to interface{} type and it works just fine.
func main(){
product := make(map[string]interface{}, 0)
product["id"] = "1234"
product["category"] = "COMPUTER"
product["price"] = 1900
fmt.Printf("%v", product)
}
However, it becomes difficult when you want to perform some data manipulation on the data whose type is defined as an interface. Since, the compiler will have no idea what the type of data is, the data should be explicitly typecasted. For example, We cannot perform any integer operation on the price of the product even though the type is an integer, this is because the type of product price here is an interface now.
func main(){
product := make(map[string]interface{}, 0)
product["id"] = "1234"
product["category"] = "COMPUTER"
product["price"] = 1900
// compiler error here,
// because the type of product["price"] is an integer
priceWithVAT := product["price"] * 13
fmt.Printf("%v", product)
fmt.Printf("%d", priceWithVAT)
}
But by asserting the type of the value to an “int” explicitly we can perform the operations.
func main() {
product := make(map[string]interface{}, 0)
product["id"] = "1234"
product["category"] = "COMPUTER"
product["price"] = 1900
price ,ok := product["price"].(int)
if !ok{
fmt.Println("error asserting the type")
return
}
priceWithVAT := price * 13
fmt.Printf("%v", product)
fmt.Printf("%d", priceWithVAT)
}
It's not always recommended to use an empty interface as a data type and it's much more understandable and performant to use regular concrete datatypes. It's even easier to perform the type-wise operations as with interface we need to assert its type before manipulating the data explicitly. Though sometimes it becomes handy when we need to create a reusable piece of code or the data type is unknown at the moment.
Conclusion
Whenever we want to reduce duplication or coupling in the existing code, it becomes easier to think in terms of interfaces. Since interfaces are just a contract, you can create any function agnostic about the type of its arguments and just care about what the object needs to do. Besides, there are several interfaces in the standard library, like io.Writer, bytes.Buffer, io.Reader etc. that are useful in reducing the boilerplate of the application.