This content originally appeared on DEV Community and was authored by Saksham Malhotra
I’ve already covered some of the basics of Go earlier. However, before diving into topics like goroutines and wait times, I think it’s important to first discuss a few key aspects of **Go **and how it’s typically used.
Closures
As we already know, Go has closures just like javascript. The only differences are:
- Strong typing make closures more predictable.
- Go closures capture variables by reference, not by value (similar to using var in JS instead of let or const).
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f()
}
Stringer
Go has a built-in interface called fmt.Stringer. When you print a value using fmt.Println, fmt.Printf, or similar functions, Go’s formatting code internally checks:
“Does this value implement the Stringer interface?”
If yes then, it calls your custom String() method to get the string representation. Else it prints the default (struct fields, numeric value, etc.).
func (p ProductID) String() string {
return fmt.Sprintf("Product-%d", p)
}
func main() {
id := ProductID(42)
fmt.Println(id) // prints: Product-42
}
Bonus: Go’s Type System Rule - Named types are distinct from their underlying types.
No method overloading
Yes, you heard it right, no more confusion arising from same-name methods. Go’s interface satisfaction is structural and implicit.
- It doesn’t care which interface you meant to implement.
- It only cares that your type’s method set matches the interface’s method signatures.
So if two interfaces have identical methods, implementing one automatically implements the other.
type A interface {
String() string
}
type B interface {
String() string
}
func (a int) String() string{
return fmt.Sprintf("%d", a)
}
var a A = 7
a.String()
The above code won’t compile because func (a int) is invalid in Go — you cannot attach a method to an unnamed type.
Corrected version:
type MyInt int
type A interface {
String() string
}
type B interface {
String() string
}
func (a MyInt) String() string{
return fmt.Sprintf("%d", a)
}
var a A = MyInt(7)
a.String();
Interface values are two-part boxes
A Go interface value is actually a pair of things:
(dynamic type, dynamic value)
Eg:
var a A = MyInt(7)
(dynamic type = MyInt, dynamic value = 7)
Unlike other languages, you’re not “creating an object of the interface”
- Instead, you’re creating a variable of interface type that stores a value (of any type) that satisfies the interface.
- The interface variable is like a box that can hold any value, as long as the value provides the required methods.
OR
Interface = Plug socket.
Any type that fits (has the right methods) can plug in; no prior agreement required.
Methods Can Belong to Any Named Type
In Go, you can attach a method to any named type, not just structs or interfaces.
type ProductID int
func (p ProductID) String() string {
return fmt.Sprintf("Product-%d", p)
}
Now, this brings us to a question:
Why do interfaces exist even though any named type can have methods?
And the answer is simple, the methods attached to a type is encapsulation, but not polymorphism.
What methods to a type can't do:
type ProductID int
func (p ProductID) String() string { return fmt.Sprintf("Product-%d", p) }
type OrderID int
func (o OrderID) String() string { return fmt.Sprintf("Order-%d", o) }
type CustomerID int
func (c CustomerID) String() string { return fmt.Sprintf("Customer-%d", c) }
Without any interface, you would need to write one function per type :
func PrintProduct(p ProductID) { fmt.Println(p.String()) }
func PrintOrder(o OrderID) { fmt.Println(o.String()) }
func PrintCustomer(c CustomerID) { fmt.Println(c.String()) }
Interfaces solve this problem:
They say, I don't care about the type as long as it knows how to do this behavior:
type Stringer interface {
String() string
}
func PrintAnything(s Stringer) {
fmt.Println(s.String())
}
PrintAnything(ProductID(1))
PrintAnything(OrderID(2))
PrintAnything(CustomerID(3))
Contract not Inheritance
The interface just describes behavior i.e. a contract, not an implementation.
type A interface {
Foo()
}
type MyType struct{}
func (m MyType) Foo() {
fmt.Println("Hello from Foo")
}
var a A = MyType{}
a.Foo()
Above thing works because MyType has a method Foo() with the same signature as required by A. It's not inheritance or anything else, but a contract.
So, if A was :
type A interface{
Foo()
Foo2()
}
and rest of the code remain same, then it will be a compiler error, since the interface contract says a to have two methods but a = MyType{} is only assigning it one.
Conclusion
Go’s design favors clarity, not magic. It doesn’t hide behavior behind inheritance or overloading; instead, it gives you small, powerful primitives: named types, methods, and interfaces.
Interfaces in Go aren’t about hierarchy but about capability. If a type can do what’s expected, it fits. That’s why Go’s polymorphism feels lightweight yet expressive.
From closures that behave predictably, to interfaces that define contracts rather than family trees, it reminds us that simplicity, when designed well, is not a limitation but a superpower.
This content originally appeared on DEV Community and was authored by Saksham Malhotra
Saksham Malhotra | Sciencx (2025-10-24T07:38:02+00:00) Go Beyond Basics: Closures, Interfaces, and Why Go Has No Inheritance. Retrieved from https://www.scien.cx/2025/10/24/go-beyond-basics-closures-interfaces-and-why-go-has-no-inheritance/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.