As someone who has worked primarily with Typescript over the last few years, learning Golang was challenging as much as it was stimulating.
Recap: What are interfaces?
Interfaces ensure consistent behavior without focusing on implementation details.
Interfaces in TypeScript are structural
TypeScript is a structurally typed type system, one way it achieves this is using interfaces.
They allow you to define the structure of objects you expect to handle within your code. Let’s take a common example of a Person
object.
interface Person {
name: string
talk(text: string): void
walk(to: string): string
}
const john: Person = {
name: 'John Doe',
talk: function (text: string): void {
console.log('Speaking: ', text)
},
walk: function (to: string): string {
return `${this.name} is at ${to}`
},
}
Go’s interfaces are behavioral
Go uses duck typing, meaning a struct matches an interface as long as it has matching method signatures.
In Go, there is no explicit implements
keyword, all structs implicitly implement interfaces. The compiler deduces this during build time.
// interface to be implemented by the Person struct
type PersonLike interface {
Walk(string) string
Talk(string)
}
// a `Person` struct
type Person struct {
Name string
}
// Talk implements PersonBehavior.
func (p Person) Talk(text string) {
println(p.Name + "is saying: '" + text)
}
// Walk implements PersonBehavior.
func (p Person) Walk(text string) string {
return p.Name + "is at " + text
}
func main() {
// declare `john` of PersonLike type
var john PersonLike
// assign an instance of the Person
john = Person{}
// invoke `.Walk` on the object
println(john.Walk("home"))
}
In the above example, a struct is first created and then the methods are implemented on it.
The compiler automatically detects the implementation of the interface by the Person struct. Any struct having the method signature will satisfy the interface type
The example below demonstrates how the same object instance can satisfy multiple interfaces, each containing a subset of its method signatures.
package main
// interface to be implemented by the Person struct
type PersonLike interface {
Walk(string) string
Talk(string)
}
// interface that only has `Walk`
type Walker interface {
Walk(string) string
}
// interface that only has `Talk`
type Talker interface {
Talk(string)
}
// a `Person` struct
type Person struct {
Name string
}
func (p Person) Talk(text string) {
println(p.Name + "is saying: '" + text)
}
func (p Person) Walk(text string) string {
return p.Name + "is at " + text
}
func main() {
// declare `john` of PersonLike type
var john PersonLike
// assign an instance of the Person
john = Person{}
var johnWalker Walker
// assign to object to `Walker` type
johnWalker = john
// invoke `.Walk`
johnWalker.Walk("home")
// Error below: johnWalker.Talk is undefined (Walker does not have Talk method)
johnWalker.Talk("I am home")
var johnTalker Talker
johnTalker = john
johnTalker.Talk("How do I get home?")
// invoke `.Walk` on the object
println(john.Walk("home")) // "John is at home"
println(john.(Person).Name) // John
}
Empty interfaces
Go allows you to create interfaces that have no methods declared i.e., it can hold a value of any type without any restrictions.
type Empty interface{}
The TypeScript equivalent would be the unknown type
const empty: unknown = {}
Empty interfaces come in handy when the programmer knows the type but it is unknown to the type system. A common example would be de-serializing unknown types.
func parse(val interface{}) (string, error) {
// Gets the type of `value`
switch value := val.(type) {
case string:
return string(value), nil
default:
return "", fmt.Errorf("unsupported data %#v", value)
}
}
Common Gotcha: Interfaces on pointer types
Methods in Go can have either Pointer receivers or value receivers. Depending on what is used the compiler treats them differently while matching interfaces.
The following code won’t be compiled because of the error in line 39.
package main
// interface that only has `Talk`
type Talker interface {
Talk(string)
}
// a `PersonPtr` struct that has a pointer receiver for `Talk`
type PersonPtr struct {
Name string
}
// Add implements PersonBehavior.
func (p *PersonPtr) Talk(text string) {
println(p.Name + "is saying: '" + text)
}
// a `Person` struct that has a value receiver for `Talk`
type Person struct {
Name string
}
// Add implements PersonBehavior.
func (p Person) Talk(text string) {
println(p.Name + "is saying: '" + text)
}
func main() {
// assign an instance of the Person
var john Talker
johnny := PersonPtr{}
johnny.Talk("I can talk")
// Error: PersonPtr does not implement Talker (method Talk has pointer receiver)
john = johnny
john.Talk("This wont work")
}
This can however be fixed by replacing the assignment that line 35 with:
john = &johnny
Why does this work?
- Since
Talk
is implemented only for*PersonPtr
, passing a pointer (&johnny
) toTalker
, ensuring it satisfies the interface.
Conclusion
- Go interfaces enable implicit implementation, reducing boilerplate and improving flexibility.
- Duck typing allows structs to satisfy interfaces automatically if method signatures match.
- Multiple interface satisfaction lets a struct conform to different behaviors dynamically.
- Empty interfaces (
interface{}
) provide a way to handle unknown types, useful for generics. - Pointer vs. value receivers impact interface satisfaction – choosing the right one is key.
Further Reading
Hope you enjoyed reading this as much as I enjoyed writing it.
If you think this will be of help to someone? Do not hesitate to share. If you liked it, tap the clap below so other people will see this here on Medium. Don’t forget to show some love by following the blog!