Interface
Interfaces in Go are the only abstract type and they are implicit interfaces
. In Go, interfaces are a powerful and flexible way to define sets of methods that types must implement
. They allow you to write functions and methods that can work with various types, as long as those types satisfy the interface contract.
And fields?
Interfaces support methods but not properties or fields
.
To declare an interface, you list the method signatures that types must implement. Any type that has methods matching these signatures implicitly implements the interface. If a type has all the required methods, it implicitly satisfies the interface
. Interfaces promote polymorphism and decoupling of code.
Declaring and Using Interfaces
package main
import "fmt"
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func printArea(s Shape) {
area := s.Area()
fmt.Printf("Area: %.2f\n", area)
}
func main() {
c := Circle{Radius: 5.0}
r := Rectangle{Width: 4.0, Height: 6.0}
printArea(c)
printArea(r)
}
Since interfaces are implicitly implemented, a concrete type does not need to declare that it implements an interface
. This behavior enables type safety and decoupling. The interface needs to be known only by the client
, nothing needs to be done in the implementation.
In Go, interfaces specify what callers need
. The client code defines the interface to specify what functionality it requires, nothing is declared on the implementation to indicate that it meets the interface
. In this way, it's possible to add a new logic provider
in the future and provide executable documentation
to ensure that any type passed into the client will match the client's need.
Interfaces can be shared also
, where it is possible to define standard interfaces like io.Reader and io.Writer. Anything that implements both interfaces, will function correctly
whether where it has been reading or writing the data.
Embedding Interfaces
Method Set for Pointer and Values Receivers
The methods defined by an interface are the method set of the interface. For struct, the method set is different depending on the pointer and values receivers functions
. The method set of a pointer instance
contains the methods defined with both pointer and values receivers, while the method set of a value instance
contains only the methods with value receivers.
In Summary
Method set for pointer instance: ALL struct's methods.
Method set for value instance: ONLY value receiver methods.
Example
Taken from this link
package main
import (
"fmt"
"time"
)
type Counter struct {
total int
lastUpdated time.Time
}
func (c *Counter) Increment() {
c.total++
c.lastUpdated = time.Now()
}
func (c Counter) String() string {
return fmt.Sprintf("total: %d, last updated: %v", c.total, c.lastUpdated)
}
type Incrementer interface {
Increment()
}
func main() {
var myStringer fmt.Stringer
var myIncrementer Incrementer
pointerCounter := &Counter{}
valueCounter := Counter{}
myStringer = pointerCounter // ok
myStringer = valueCounter // ok
myIncrementer = pointerCounter // ok
myIncrementer = valueCounter // compile-time error!
fmt.Println(myStringer, myIncrementer)
}
How Interfaces are Implemented, Nil Interfaces and Comparable
Interfaces are implemented as a struct with two pointer fields
. One pointer is for the value
and one is for the type
of the value. Both type and value must be nil for an interface to be considered nil
.
Interface Structure Image
Interfaces are comparable
and two instances of an interface type are equal only if their types and their values are equal
. However, if comparing two instances of an interface that the types aren't comparable like a slice, this will trigger a panic at runtime.
Comparing Interfaces
Be careful when using ==
or !=
with interfaces or using an interface as a map key
, since it's easy to generate panic in runtime if the type is not comparable.
Comparing Interfaces
$ go run src/interface/interface_compare.go
true
false
false
panic: runtime error: comparing uncomparable type main.DoubleIntSlice
goroutine 1 [running]:
main.DoublerCompare(...)
/home/romerin/Projects/mastering-go/src/interface_compare.go:10
main.main()
/home/romerin/Projects/mastering-go/src/interface_compare.go:35 +0x190
exit status 2
package main
import "fmt"
type Doubler interface {
Double()
}
func DoublerCompare(d1, d2 Doubler) {
fmt.Println(d1 == d2)
}
type DoubleInt int
func (d *DoubleInt) Double() {
*d = *d * 2
}
type DoubleIntSlice []int
func (d DoubleIntSlice) Double() {
for i := range d {
d[i] = d[i] * 2
}
}
func main() {
var di DoubleInt = 10
var di2 DoubleInt = 10
var dis = DoubleIntSlice{1, 2, 3}
var dis2 = DoubleIntSlice{1, 2, 3}
DoublerCompare(&di, &di)
DoublerCompare(&di, &di2) // two diff pointers
DoublerCompare(&di, dis)
DoublerCompare(dis, dis2)
}
Performance
As discussed in Less Pointers, Less Work for Garbage Collector, reducing heap allocations improves performance by reducing the amount of work for the garbage collector. Using structs avoids heap allocation, and invoking a function with parameters of interface types triggers heap allocation for each interface parameter
.
About Performance
Figuring out the trade-off between better abstraction and better performance should be done over the life of your program.
Passing Interface to Concrete Type
Go provides two ways to see if a variable of an interface type has a specific concrete type or if the concrete type implements another interface. This is not the same thing than type conversion, conversions change a value to a new type, while assertions reveal the type of the value stored in the interface
. Type assertions work only with interfaces while type conversions work with concrete types and interfaces.
Warning
These techniques should be used when necessary. It's better to be clear with inputs and returns values.
Type Assertions
Type assertions provide one way to check if a variable of an interface type has a specific concrete type or if the concrete type implements another interface
. Since Go is very careful about concrete types, even if two types share an underlying type, a type assertion must match the type of the value stored in the interface.
If a type assertion gets wrong, the code panics. To avoid that, it's possible to use the comma ok idiom
. All types of assertions are checked at runtime
, so any error that can happen will be at runtime with panic if the comma ok idiom is not used.
Using type assertion
package main
import "fmt"
type MyInt int
func main() {
var anyVar any
var mInt MyInt = 20
anyVar = mInt
asser_int := anyVar.(MyInt)
fmt.Println(asser_int + 1)
assert_wrong, ok := anyVar.(string)
if !ok {
fmt.Println("Error")
} else {
// It will never print because this just happens in runtime and not compile time
// Runtime error: panic: interface conversion: interface {} is main.MyInt, not string
fmt.Println("THIS WILL NEVER PRINT: ", assert_wrong)
}
}
Type Switches
Type switches provide another way to check if an interface could be one of multiple possible types
. It's possible to assign the variable being checked to another variable valid only within the switch or shadowing the variable. The type of the new variable depends on which case matches.
Using type switch
func doThings(i any) {
switch j := i.(type) {
case nil:
// j is any
case int:
// j is of type int
case MyInt:
// j is of type MyInt
case io.Reader:
// j is of type io.Reader
case string:
// j is a string
case bool, rune:
// i is either a bool or rune, so j is of type any
default:
// no idea what i is, so j is of type any
}
}
Checking Others Interfaces Implementation - Optional Interfaces
Using type assertions and type switches can be used to check if the concrete type behind the interface also implements another interface
. This allow to specify optional interfaces.
Checking multiple Interfaces
// WriterTo is the interface that wraps the WriteTo method.
//
// WriteTo writes data to w until there's no more data to write or
// when an error occurs. The return value n is the number of bytes
// written. Any error encountered during the write is also returned.
//
// The Copy function uses WriterTo if available.
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rt, ok := dst.(ReaderFrom); ok {
return rt.ReadFrom(src)
}
// function continues...
}
Interfaces and Function Params
Go encourages the usage of small interfaces but functions in Go are first-class concepts. Both approaches can be used since an interface of only one method could replace a parameter of a function type.
If it's a simple function, then a parameter of a function type is a good choice
. However, if the function is likely to depend on many other functions or other states that are not specified in its input parameters
, use an interface parameter with a defined function.
Use Case Example - HTTP Handler
The HTTP Handler interface is defined in this way:
The ServerHTTP
function allows functions to implement the interface
. Any function with the same signature then the ServerHTTP function can be used as http.Handler. This allows the implementation of HTTP handlers using functions, methods, or closures.