07. Abstractions

Methods

Go does not have classes. However, you can define methods on types. A method is a function with a special receiver argument. The receiver appears in its own argument list between the func keyword and the method name.

// Age and int are two distinct types. We
// can't declare methods for int and *int,
// but can for Age and *Age.
type Age int
func (age Age) LargerThan(a Age) bool {
	return age > a
}
func (age *Age) Increase() {
	*age++
}



type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X * v.X + v.Y * v.Y)
}

v := Vertex{3, 4}
fmt.Println(v.Abs())

You can declare a method on non-struct types, too.

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())

One major difference between functions and methods is we can have multiple methods with same name while no two functions with the same name can be defined in a package. We are allowed to create methods with same name as long as their receivers are different.

Pointer receiver methods

So far, we have seen methods belong to a type. But a method can also belong to the pointer of a type. When a method belongs to a type, its receiver receives a copy of the object on which it was called.

Methods with pointer receivers can modify the value to which the receiver points. Since methods often need to modify their receiver, pointer receivers are more common than value receivers.

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

v := Vertex{3, 4}
v.Scale(10) // {30 40}

There are two reasons to use a pointer receiver:

  • The first is so that the method can modify the value that its receiver points to.

  • The second is to avoid copying the value on each method call. This can be more efficient if the receiver is a large struct, for example.

  • In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers. This rule arises because pointer methods can modify the receiver; invoking them on a value would cause the method to receive a copy of the value, so any modifications would be discarded.

  • Like promoted fields, methods implemented by the anonymously nested struct are also promoted to the parent struct.

  • We can define a method with value or pointer receiver and call it on pointer or value. Go does the job of type conversion behind the scenes as we’ve seen in the earlier examples.

  • Hence a method can receive any type as long as the type definition and method definition is in the same package.

Можно вызывать методы на nil, panic не будет выброшен если в методе есть проверка на nil.

Pointer or Value receiver ?

We can always declare methods with pointer receivers without any logic problems. It is just a matter of program performance that sometimes it is better to declare methods with value receivers. For the cases value receivers and pointer receivers are both acceptable, here are some factors needed to be considered to make decisions.

  • Too many pointer copies may cause heavier workload for garbage collector.

  • If the size of a value receiver type is large, then the receiver argument copy cost may be not negligible. Pointer types are all small-size types.

  • Declaring methods of both value receivers and pointer receivers for the same base type is more likely to cause data races if the declared methods are called concurrently in multiple goroutines.

  • Values of the types in the sync standard package should not be copied, so defining methods with value receivers for struct types which embedding the types in the sync standard package is problematic.

Receiver Arguments Are Passed by Copy

Same as general function arguments, the receiver arguments are also passed by copy. So, the modifications on the direct part of a receiver argument in a method call will not be reflected to the outside of the method.

import "fmt"

type Book struct {
	pages int
}

func (b Book) SetPages(pages int) {
	b.pages = pages
}

func main() {
	var b Book
	b.SetPages(123)
	fmt.Println(b.pages) // 0
}

Methods are functions

We can also assign the method to a variable or pass it to a parameter of type func(int)int. This is called a method value:

type Adder struct { 
    start int
}
func (a Adder) AddTo(val int) int { 
    return a.start + val
}

myAdder := Adder{start: 10} 
fmt.Println(myAdder.AddTo(5)) // prints 15

A method value is a bit like a closure, since it can access the values in the fields of the instance from which it was created.

You can also create a function from the type itself. This is called a method expression:

f2 := Adder.AddTo
fmt.Println(f2(myAdder, 15)) // prints 25

In the case of a method expression, the first parameter is the receiver for the method our function signature is func(Adder, int) int.

Interfaces

  • Types Are Executable Documentation

  • You’ll often hear experienced Go developers say that your code should “Accept interfaces, return structs. Rather than writing a single factory function that returns different instances behind an interface based on input parameters, try to write separate factory functions for each concrete type.

An interface type is defined as a set of method signatures. A value of interface type can hold any value that implements those methods.

type I interface {
    M()
}

type T struct {
    S string
}

// This method means type T implements the interface I,
// but we don't need to explicitly declare that it does so.
func (t T) M() {
    fmt.Println(t.S)
}

var i I = T{"hello"}
i.M()

A type implements an interface by implementing its methods. Implicit interfaces decouple the definition of an interface from its implementation, which could then appear in any package without prearrangement.

  • There is no explicit declaration of intent, no "implements" keyword.

  • Interface definition does not prescribe whether an implementor should implement the interface using a pointer receiver or a value receiver

  • Implementing interface in Go method types must match exactly

  • An interface type always implements itself.

  • Two interface types with the same method set implement each other.

For example, in the following example, the method sets of struct pointer type *Book, integer type MyInt and pointer type *MyInt all contain the method prototype About() string, so they all implement the above mentioned interface type interface {About() string}.

type Book struct {
	name string
	// more other fields ...
}

func (book *Book) About() string {
	return "Book: " + book.name
}

type MyInt int

func (MyInt) About() string {
	return "I'm a custom integer value"
}

type 

Interface structure

Under the covers, interface values can be thought of as a tuple of a value and a concrete type: (value, type). An interface value holds a value of a specific underlying concrete type. Calling a method on an interface value executes the method of the same name on its underlying type.

If the concrete value inside the interface itself is nil, the method will be called with a nil receiver. In some languages this would trigger a null pointer exception, but in Go it is common to write methods that gracefully handle being called with a nil receiver (as with the method M in this example.)

type I interface {
    M()
}

type T struct {
    S string
}

func (t *T) M() {
    if t == nil {
        fmt.Println("<nil>")
        return
    }
    fmt.Println(t.S)
}

var i I
var t *T
i = t // (<nil>, *main.T)
i.M() // <nil>

nil interface value

In order for an interface to be considered nil both the type and the value must be nil. In the Go runtime, interfaces are implemented as a pair of pointers, one to the underlying type and one to the underlying value. What nil indicates for an interface is whether or not you can invoke methods on it.

import (
    "fmt"
)

type ISayHi interface {
    Say()
}

type SayHi struct{}

func (s *SayHi) Say() {
    fmt.Println("Hi!")
}

func main() {
    // at this point variable “sayer” only has the static type of ISayHi
    // dynamic type and value are nil
    var sayer ISayHi

    // as expected sayer equals to nil
    fmt.Println(sayer == nil) // true

    // a nil variable of a concrete type
    var sayerImplementation *SayHi

    // dynamic type of the interface variable is now SayHi
    // the actual value interface points to is still nil
    sayer = sayerImplementation

    // sayer no longer equals to nil, because its dynamic type is set
    // even though the value it points to is nil
    // which isn't what most people would expect here
    fmt.Println(sayer == nil) // false
}

A nil interface value holds neither value nor concrete type. Calling a method on a nil interface is a run-time error because there is no type inside the interface tuple to indicate which concrete method to call.

var i I // (<nil>, <nil>)
i.M() // panic: runtime error

Empty interface (Any type)

The interface type that specifies zero methods is known as the empty interface:

var i interface{}
i = 42
i = "hello"

An empty interface may hold values of any type. (Every type implements at least zero methods.) Empty interfaces are used by code that handles values of unknown type. For example, fmt.Print takes any number of arguments of type interface{}.

Embedding for interfaces

Interface embedding is strightforward:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

Only interfaces can be embedded within interfaces.А

Embedded interfaces into struct

Интерфейсы могут быть встроены в структуры: тогда при создании структуры нужно будет передать структуру реализующую интерфейс.

type iStuff interface {
    DoStuff()
}

type realStuff string

func (r realStuff) DoStuff() {
    fmt.Println(r)
}

type stuff struct {
    iStuff
    Name string
}

func (s stuff) SomeComplex() {
    s.DoStuff()
}

func main() {
    r := realStuff("Hey")
    rS := stuff{r, "stuff"}
    rS.SomeComplex()
}

Functions can implement interfaces

The most common usage is for HTTP handlers. An HTTP handler processes an HTTP server request. It’s defined by an interface:

type Handler interface { 
    ServeHTTP(http.ResponseWriter, *http.Request)
}

By using a type conversion to http.HandlerFunc, any function that has the signature func(http.ResponseWriter,*http.Request) can be used as an http.Handler:

type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    f(w, r) 
}

This lets you implement HTTP handlers using functions, methods, or closures using the exact same code path as the one used for other types that meet the http.Handler interface.

Type assertions

A type assertion provides access to an interface value's underlying concrete value.

t := i.(T)

This statement asserts that the interface value i holds the concrete type T and assigns the underlying T value to the variable t. If i does not hold a T, the statement will trigger a panic.

To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.

t, ok := i.(T)

If i holds a T, then t will be the underlying value and ok will be true. If not, ok will be false and t will be the zero value of type T, and no panic occurs.

When an interface could be one of multiple possible types, use a type switch instead:


func doThings(i interface{}) { 
    switch j := i.(type) { 
    case nil:
        // i is nil, type of j is interface{}
    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 interface{}
    default:
        // no idea what i is, so j is of type interface{}
    } 
}

Check interface implementation

You can ask the compiler to check that the type T implements the interface I by attempting an assignment using the zero value for T or pointer to T, as appropriate:

type T struct{}
var _ I = T{}       // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I.

Exporting

If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself. Exporting just the interface makes it clear the value has no interesting behavior beyond what is described in the interface. It also avoids the need to repeat the documentation on every instance of a common method.

In such cases, the constructor should return an interface value rather than the implementing type.

Compare Interfaces

Two interfaces can be compared with == and != operators. Two interfaces are always equal if the underlying dynamic values are nil, which means, two nil interfaces are always equal, hence == operation returns true.

var a, b interface{}
fmt.Println( a == b ) // true

If these interfaces are not nil, then their dynamic types (the type of their concrete values) should be the same and the concrete values should be equal.

If the dynamic types of the interface are not comparable, like for example, slice, map and function, or the concrete value of an interface is a complex data structure like slice or array that contains these uncomparable values, then == or != operations will result in a runtime panic.

If one interface is nil, then == operation will always return false.

Assigning

If a type T implements an interface type I, then any value of type T can be implicitly converted to type I. In other words, any value of type T is assignable to (modifiable) values of type I. When a T value is converted (assigned) to an I value,

  • if type T is a non-interface type, then a copy of the T value is boxed (or encapsulated) into the result (or destination) I value. The time complexity of the copy is O(n), where n is the size of copied T value.

  • if type T is also an interface type, then a copy of the value boxed in the T value is boxed (or encapsulated) into the result (or destination) I value. The standard Go compiler makes an optimization here, so the time complexity of the copy is O(1), instead of O(n).

When a value is boxed in an interface value, the value is called the dynamic value of the interface value. The type of the dynamic value is called the dynamic type of the interface value.

The direct part of the dynamic value of an interface value is immutable, though we can replace the dynamic value of an interface value with another dynamic value.

Type Map

Go compilers will build a global table which contains the information of each type at compile time. The information includes what kind a type is, what methods and fields a type owns, what the element type of a container type is, type sizes, etc. The global table will be loaded into memory when a program starts.

At run time, when a non-interface value is boxed into an interface value, the Go runtime (at least for the standard Go runtime) will analyse and build the implementation information for the type pair of the two values, and store the implementation information in the interface value. The implementation information for each non-interface type and interface type pair will only be built once and cached in a global map for execution efficiency consideration. The number of entries of the global map never decreases. In fact, a non-nil interface value just uses an internal pointer field which references a cached implementation information entry. The implementation information for each (interface type, dynamic type) pair includes two pieces of information:

  1. the information of the dynamic type (a non-interface type)

  2. and a method table (a slice) which stores all the corresponding methods specified by the interface type and declared for the non-interface type (the dynamic type).

These two pieces of information are essential for implementing two important features in Go:

  1. The dynamic type information is the key to implement reflection in Go.

  2. The method table information is the key to implement polymorphism.

Interface Design

  • Prefer small interfaces

  • Don't design interfaces, discover them

  • Interfaces on the client side

  • Functions: Returning structs instead of interfaces and accepting interfaces if possible.

Last updated