Reputation: 285
I've recently taken a liking to the Go programming language, I've found it wonderful so far but am really struggling to understand interfaces. I've read about quite a bit about them, but they still seem very abstract to me.
I've wrote a quick bit of code that uses an interface below:
package main
import (
"fmt"
"math"
)
type Circer interface {
Circ() float64
}
type Square struct {
side float64
}
type Circle struct {
diam, rad float64
}
func (s *Square) Circ() float64 {
return s.side * 4
}
func (c *Circle) Circ() float64 {
return c.diam * math.Pi
}
func (c *Circle) Area() float64 {
if c.rad == 0 {
var rad = c.diam / 2
return (rad*rad) * math.Pi
} else {
return (c.rad*c.rad) * math.Pi
}
}
func main() {
var s = new(Square)
var c = new(Circle)
s.side = 2
c.diam = 10
var i Circer = s
fmt.Println("Square Circ: ", i.Circ())
i = c
fmt.Println("Circle Circ: ", i.Circ())
}
I can't really see the purpose of the Circer interface. The methods are already written and I could save two lines of code by simply calling them directly on the structs, rather than using Circer as a wrapper.
Is there something I'm missing? Am I using the interface incorrectly? Any help or examples are appreciated.
Upvotes: 1
Views: 325
Reputation: 20125
"We demand rigidly defined areas of doubt and uncertainty!" - Douglas Adams, The Hitchhiker's Guide to the Galaxy
In order to understand Interfaces in Go, we must first understand why we program to Interfaces in general.
We use interfaces to hide implementation details behind an abstraction. We like to hide these details because the details (i.e. the how) are more likely to change than the abstraction and because it allows us to extend and change our applications without having the change ripple throughout our programs. When a consumer depends on an Interface instead of a concrete type, they are decoupling their program from the implementation details behind the Interface which shelters the consumer from change and makes it easier to test, extend, and maintain their application.
Go has a very powerful Interface implementation. As in most languages, it provides a way to specify the behavior of an object through an abstraction so that any place the abstraction is used any implementation of that abstraction can be used, but in Go there is no need to explicitly declare that your concretion implements a given Interface as Go handles this automatically.
Removing the explicit declaration requirement has interesting ramifications, such as: you can let your program surface the Interfaces as you go to help you identify the appropriate abstractions, without needing to annotate all of the implementations as you discover them. This also means Interfaces that are created for Testing does not need to pollute your implementation code. Additionally, there isn't an explicit relationship between the Interface and the implementer, so the implementer has no dependency/coupling in that direction.
In the example you've provided it is certainly simpler and easier to avoid the complexity and "cognitive load" of using an Interface instead of binding to the implementation (concretion). In most trivial examples it can seem like dogmatic over engineering to use an interface.
Interfaces are a powerful way for us to decouple our applications to make it easier for them to grow over time. If you anticipate change/variation (and need to protect your application from that change/variation) then creating and depending on an Interface is a step in the right direction.
For more...
See this "good" Go Object Oriented Design post.
And take a look at the SOLID design principles, as it is an excellent place to start when considering the implications of abstraction and managing dependencies and change.
Upvotes: 2
Reputation: 12023
What you're missing are scenarios where you can't know statically what kind of thing you have on hand. Let's get concrete.
Think of io.Reader
, for example. There are lots of things that implement the read
method of the interface. Say that you write a program that uses io.Reader
. For example, a program might print an MD5-sum of the content in a io.Reader
.
package mypackage
import (
"fmt"
"crypto/md5"
"io"
"strings"
)
func PrintHashsum(thing io.Reader) {
hash := md5.New()
io.Copy(hash, thing)
fmt.Println("The hash sum is:", hash.Sum(nil))
}
and say that you use this mypackage
in another file elsewhere:
func main() {
mypackage.PrintHashsum(strings.NewReader("Hello world"))
}
Now say that you use an implementation of an io.Reader
that decompresses zip files on the fly, such as the one in the archive/zip
package.
import "archive/zip"
// ...
func main() {
// ...
anotherReader = zip.NewReader(...)
// ...
}
Due to how interfaces work, you can feed such a zip-sourced reader into the MD5-sum computing mypackage.PrintHashsum
function without doing anything else to its existing code or recompiling mypackage
!
func main() {
// ...
anotherReader = zip.NewReader(...)
mypackage.PrintHashsum(anotherReader)
}
Interfaces have everything to do with letting programs be open to dynamic extension. In your example, you might argue that the compiler should just know exactly what method should be called. But in cases where your compiler supports separate compilation (like Go) for speed, the compiler can't possibly know: at the point of compiling mypackage
, the compiler can't see all the possible implementations of io.Reader
: it's not a mind reader or time traveler!
Upvotes: 4
Reputation: 54079
The point of interfaces is that you can make general purpose functions like ShowMeTheCircumference
below.
package main
import (
"fmt"
"math"
)
type Circer interface {
Circ() float64
}
type Square struct {
side float64
}
type Circle struct {
diam, rad float64
}
func (s *Square) Circ() float64 {
return s.side * 4
}
func (c *Circle) Circ() float64 {
return c.diam * math.Pi
}
func ShowMeTheCircumference(name string, shape Circer) {
fmt.Printf("Circumference of %s is %f\n", name, shape.Circ())
}
func main() {
square := &Square{side: 2}
circle := &Circle{diam: 10}
ShowMeTheCircumference("square", square)
ShowMeTheCircumference("circle", circle)
}
Upvotes: 8