Go Interfaces and Duck Types
What is a “duck type”?
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
Traditional static languages such as Java and C++ must show that the type implements an interface before it can be used anywhere that requires that interface, otherwise it won’t compile, which is why static languages are safer than dynamic languages. But Go, as a “modern” static language, uses the object inference strategy of dynamic programming languages, which is more concerned with how objects can be used than with the types of objects themselves. In other words, Go introduces the convenience of a dynamic language while performing the type checking of a static language, so it adopts a compromise: instead of requiring the type to be declared as implementing an interface, the compiler can detect it as long as it implements the relevant methods.
As an example, the following code snippet first defines an interface, and uses this interface as a parameter to the function.
Next, two more structures are defined.
Finally, the sayHello()
function is called in the main()
function.
The output after running the program is as follows.
In the main
function, the call to the sayHello()
function passes in a
and b
objects, which are not explicitly declared to implement the IGreeting
type, but only the greeting()
function as specified by the interface. The compiler then implicitly converts the a
and b
objects to the IGreeting
type when it calls the sayHello()
function, which is a static language type checking feature.
As you can see, duck typing is a dynamic style of language in which the effective semantics of an object is determined not by inheritance from a particular class or implementation of a particular interface, but by its “current set of methods and properties”. The Go language, as a static language, implements “duck types” through interfaces, but in fact the Go compiler does the implicit conversion work.
Value receivers and pointer receivers
In Go, everything is a type, including the primitive types int, bool, string, as well as the built-in types slice, map, and even functions, in a way similar to javascript.
The difference between a method and a function is that a method has a receiver, and by adding a receiver to a function, it becomes a method. The receiver can be either a value receiver or a pointer receiver.
In general, when calling a method, the value type can call both the value receiver’s method and the pointer receiver’s method; the pointer type can call both the pointer receiver’s method and the value receiver’s method. That is, regardless of whether the method’s receiver is a value type or a pointer type, both values and pointers of that type can call the method without having to strictly conform to the receiver’s type.
Let’s see an example.
|
|
The result of running the program is as follows.
After calling the Scale()
method, its value changes regardless of whether the caller is a value type or a pointer type. In fact, when the type and the receiver type of the method are different, the compiler is actually doing some work behind the scenes, which can be presented in a table as follows.
- | Receiver is also a value type | call receiver is also a pointer type |
---|---|---|
Value Type | A copy of the method caller, similar to “passing a value” | Using the “reference” (address) of the value to call the method, v.Scale(10) in the above example actually translates implicitly to (&v).Scale(10) |
Pointer Type | Pointer is implicitly dereferenced to “value”, in the above example p.Abs() is actually implicitly translated to (*p).Abs() |
In fact, it is also a “value pass”, where the operation inside the method directly affects the caller, similar to a pointer passing parameters |
So why can values and pointers of that type call this method regardless of the type of the method’s receiver? It’s actually Go’s syntactic sugar at work.
Key point: Implementing a method whose receiver is a value type is equivalent to automatically implementing a method whose receiver is a pointer type; and implementing a method whose receiver is a pointer type does not automatically generate a method whose corresponding receiver is a value type.
The meaning of the above statement can be understood by looking at a simple example.
|
|
The program runs with the following results.
But if you change the first statement of the main()
function.
It will error after running.
The difference between the two codes is that the first assigns &ReadWriter{"hello"}
to a variable of type iReadWriter
; the second assigns ReadWriter{"hello"}
to a variable of type iReadWriter
.
The reason for the error in the second code is that the ReadWriter
type does not implement the iReadWriter
interface, which means that the ReadWriter
type does not implement the write()
method; on the surface, the *ReadWriter
type does not implement the read()
method either, but since the ReadWriter
type implements the read()
method, so that the *ReadWriter
type automatically has (implicitly implements) the read()
method.
That is, a method whose receiver is a pointer type is likely to change the operation of the receiver’s properties in the method, thus affecting the receiver, while for a method whose receiver is a value type, there will be no effect on the receiver itself in the method. So, when a method whose receiver is a value type is implemented, a method whose receiver is the corresponding pointer type can be automatically generated, because neither will affect the receiver. However, when a method whose receiver is a pointer type is implemented, if a method whose receiver is a value type is automatically generated at this point, the change to the receiver that was expected (achieved by a pointer) cannot now be achieved, because the value type will produce a copy that will not really affect the caller.
Value receiver or pointer receiver
If the receiver of a method is of type value, it is the copy of the object that is modified without affecting the caller, regardless of whether the caller is an object or a pointer to an object; if the receiver of a method is of type pointer, the caller modifies the object itself pointed to by the pointer.
Reasons for using a pointer as the receiver of a method.
- the ability of the method to modify the value pointed to by the receiver.
- to avoid copying the value each time the method is called, which is more efficient when the type of the value is a large structure.
Whether to use a value receiver or a pointer receiver is not determined by whether the method modifies the caller (that is, the receiver), but should be based on the nature of the type.
If the type has a “primitive nature”, i.e. its members are all primitive types built into the Go language, such as strings, integer values, etc., then define the methods of the value receiver type; for built-in reference types, such as slice, map, channel, etc., which are special and are declared is actually creating a header
. For them it is also better to define the methods of the value recipient type directly. This way, when the function is called, it is a direct copy of the header
of these types, which itself is designed for copying. If the types have a non-primitive nature and cannot be copied safely, and such types should always be shared, then define the methods of the pointer receiver. For example, a file structure defined inside the Go language standard library should not be copied and should have only one entity.
Composition and Inheritance
Everything in Go is a type, including the built-in primitive types in Go, such as strings and integers, as well as the built-in reference types, such as slice, map, interfac, channel, func, and so on, and of course user-defined types. This is a bit like the concept of object-oriented, but not exactly the same, but especially similar to the syntactic sugar of javascript.
So how does Go implement “inheritance” like in object-oriented languages like Java? The answer is that the Go language uses “combinations” to implement inheritance-like concepts. As an example.
|
|
The output of the program is as follows.
As you can see, the age property of animal
is combined into cat
, which becomes the property of cat
, and the relationship between animal
and cat
is inherited through the combination. But when animal
’s property and cat
’s property have the same name, the property of the combiner will override the property of the combinee with the same name, i.e. here cat
’s name property will override animal
’s name property, and to access animal
’s name you need to access it indirectly through the combinee, i.e. you need to access cat.animal.name
to access animal
’s own name property.
Similar to objects defined in structs, interface objects can be combined with other interface objects in a way that is equivalent to adding methods of other interfaces to the interface. As an example.
|
|
If structure type A implements all the methods required by the interface, the object of structure type A can be assigned to the corresponding interface; and if another structure B combines the previous structure A, then structure B also implements all the methods of the interface, so the object of structure B can be assigned to the interface type. Go implements the concept of inheritance in object-oriented languages through this combination.
The Go language does this precisely by providing the mechanism of aliased structure plus interface combinations, allowing one structure/interface to contain anonymous members of another structure/interface type, so that the nested x.d.e.f
members of the anonymous member chain can be accessed by the simple dot operator x.f
. By the same rule, methods with embedded anonymous member chains are promoted to methods of external types.
However, it is important to note that
-
Composition a type, the method of this internal type becomes a method of the external type, but when it is called, the receiver of the method is the internal type (the type being combined), not the external type.
In the above example, even though the combination is called as
job.Log(...)
, the receiver of theLog
method is still thelog.Logger
pointer, so it is not possible to access other member methods and variables ofjob
in theLog
method. -
The type being combined is not a base class
If you are familiar with inheritance in “classes” in traditional object-oriented languages, you may be tempted to think of the “combined type” as a base class and the “external type” as its subclass or inheritance class, or to think of the “external type” as “is a” type, but this is not correct.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
type Point struct{ X, Y float64 } type ColoredPoint struct { Point Color color.RGBA } func (p Point) Distance(q Point) float64 { dX := q.X - p.X dY := q.Y - p.Y return math.Sqrt(dX*dX + dY*dY) } red := color.RGBA{255, 0, 0, 255} blue := color.RGBA{0, 0, 255, 255} var p = ColoredPoint{Point{1, 1}, red} var q = ColoredPoint{Point{5, 4}, blue} fmt.Println(p.Distance(q.Point)) // "5" p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
Note the call to the
Distance
method in the above example. TheDistance
method has a parameter of typePoint
, butq
is not aPoint
class, so even thoughq
has the combined typePoint
, we have to explicitly select it, and trying to passq
directly will result in an error.In fact, considering the problem from an implementation perspective, the inline fields direct the compiler to generate additional wrapper methods to delegate to the already declared methods, which is equivalent to the following form.
When
Point.Distance()
is called by the above compiler-generated wrapper method, its receiver value is stillp.Point
, notp
. -
Anonymous conflicts and implicit names
Anonymous members also have an implicit name, with their type name (minus the package name part) as the name of the member variable. So you cannot have two anonymous members of the same type at the same level at the same time, which would lead to name conflicts.
Both of the following points indirectly indicate that anonymous combinations are not inheritance.
- anonymous members have implicit names
- anonymous may conflict (duplex field)