What is an Interface in Golang?
pyramidskeme on June 25, 2025, 12:09 PMOk, so first off... Just in case there's any confusion, this post is NOT about application user interfaces or "UIs", it's about the concept of using Interfaces in programming languages. Maybe you've run across them before while reading through some code and wondered what they're for. Or maybe you knew but forgot. Anyway, here's a simple explanation using Golang.
You'll only notice these in "strongly typed" languages because they're essentially a way for you to have families of structs that adhere to a certain set of rules. The reason something like this is necessary is that, since your functions must declare what types of variables you pass to them, it's pretty common to end up in a situation where you have a function with some logic that could be re-usable, but isn't because it only accepts a specific type. A fancier way of saying this is that Interfaces allow for "polymorphism". You can think of Polymorphism as basically meaning a function can morph to accommodate slightly different types as long as the types behave similarly enough. If those types fit that description, we declare an Interface and say those types "implement" that interface.
One important thing to note about Interfaces in Golang, is that they only specify the functionality of a a struct, not the data properties that it can hold. This is different than what you would see in Typescript, for example. In other words, an interface can only specify methods.
So just say you will have a function that takes a struct "Car", and you defined that type like this:
type Car struct {
Make string
Model string
DoorCount int16
}
And you define a method on it via a receiver
fn (c *Car) Accelerate () {
// code that makes it go
}
Then you have a function that accepts the Car type:
func driveCar (c Car) {
c.Accelerate()
}
Ok, this is all well and good! But what if you later start adding other struct types that are similar to cars with similar functions. For example now you want to add motorcycles:
type Motorcycle struct {
Make string
Model string
HandleBarLength float32
}
fn (m * Motorcycle) Accelerate () {
// code that makes it go
}
Then you want to have a function that drives a Motorcycle... you obviously can't use driveCar
because it will only accept variables of type "Car". Motorcycle
and Car
are similar in that they both have an Accelerate
method, but different in that Car
has DoorCount
and Motorcycle
has HandleBarLength
. So you would have to create another function that's duplicating pretty much all of the functionality of driveCar
:
func driveMotorcycle (m Motorcycle) {
m.Accelerate()
}
Ok... This is fine...
But what about when you need to do one for trucks? suvs? boats? etc? Code duplication galore. Here's where Interfaces come to the rescue:
Instead of having multiple functions for all of your different types, you can create an interface called "Vehicle" and as long as your structs have the same methods on them as that "Vehicle" interface declares, the compiler will implement that interface on them. Now, you can make a function that more generically accepts the Vehicle interface so that it can be re-used with different types of vehicles. So, to do that, we would write this:
type Vehicle interface {
Accelerate()
}
Now, we can get rid of the driveCar
and driveMotorcycle
functions and just have one called driveVehicle
that accepts any type that implements the Vehicle interface.
func driveVehicle (v Vehicle) {
v.Accelerate()
}
Now we're able to do all of this:
driveVehicle(car)
driveVehicle(motorcycle)
driveVehicle(truck)
driveVehicle(boat)
driveVehicle(jetSki)
// and with whatever other types your heart desires as long as they implement the vehicle interface
One other cool (and potentially dangerous) thing about interfaces is that you can declare empty interfaces as well. An empty interface is one that has no methods defined in it. What this means is that you can basically declare a function that accepts an empty interface... which means it will accept anything because anything you pass to it will technically satisfy the conditions of having "no specific methods". Here's an example:
func acceptAnything (i interface{}) { // will accept any type of argument you pass in
fmt.Printf("%T\n", i) // will print the type of whatever you pass in
}
There are some cases where you would want to allow a function to freely accept anything passed in, but you have to be careful when doing this because you're basically bypassing the type safety.
One case where it's useful to be able to use an empty interface is when you're passing some data to a front end templating system, and you don't necessarily know what the shape of that data will look like because it's shape will depend on the specific data to be displayed and on what page it will be displayed on. For example, if you wanted to avoid code duplication (which you should) and use the same template function for all your pages:
// we don't always know what the data for a specific page template will look like,
// but we want to use the same renderTemplate function for all page templates
func renderTemplate(w http.ResponseWriter, page string, data interface{}) {
// some code to get the template for a page and pass data to it
}
// calling this function with a list of users
renderTemplate(w, "user-list", map[string]interface{}{
"Title": "List of Users",
"Content": "These are all the users",
"Users": users,
})
// calling this function with a list of blog posts
renderTemplate(w, "blog-posts", map[string]interface{}{
"Title": "Blog Posts",
"Posts": posts,
})
There's more that you can do with Interfaces in Go, but that's the general idea.