07. Abstractions
Last updated
Was this helpful?
Last updated
Was this helpful?
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.
You can declare a method on non-struct types, too.
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.
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.
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
.
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.
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.
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:
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:
In the case of a method expression, the first parameter is the receiver for the method our function signature is func(Adder, int) int
.
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.
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}
.
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.)
nil
interface valueIn 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.
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.
The interface type that specifies zero methods is known as the empty interface:
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{}
.
Interface embedding is strightforward:
Only interfaces can be embedded within interfaces.А
Интерфейсы могут быть встроены в структуры: тогда при создании структуры нужно будет передать структуру реализующую интерфейс.
The most common usage is for HTTP handlers. An HTTP handler processes an HTTP server request. It’s defined by an interface:
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
:
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.
A type assertion provides access to an interface value's underlying concrete value.
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.
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:
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:
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.
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
.
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
.
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.
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:
the information of the dynamic type (a non-interface type)
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:
The dynamic type information is the key to implement reflection in Go.
The method table information is the key to implement polymorphism.
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.