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.

1
2
3
4
5
6
7
type IGreeting interface {
    greeting()
}

func sayHello(i IGreeting) {
    i.greeting()
}

Next, two more structures are defined.

1
2
3
4
5
6
7
8
9
type A struct {}
func (a A) greeting() {
    fmt.Println("Hi, I am A!")
}

type B struct {}
func (b B) greeting() {
    fmt.Println("Hi, I am B!")
}

Finally, the sayHello() function is called in the main() function.

1
2
3
4
5
6
7
func main() {
    a := A{}
    b := B{}

    sayHello(a)
    sayHello(b)
}

The output after running the program is as follows.

1
2
Hi, I am A!
Hi, I am B!

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Vertex struct {
    X, Y float64
}

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

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

func main() {
    // v is the value type
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
    // Value type, calling a method whose receiver is also a pointer type
    v.Scale(10)
    // Value type, call a method whose receiver is also a value type
    fmt.Println(v.Abs())

    // p is a pointer type
    p := &Vertex{4, 3}
    // Pointer type, calling a method whose receiver is a value type
    fmt.Println(p.Abs())
    // Pointer type, call a method whose receiver is also a pointer type
    p.Scale(10)
    fmt.Println(p.Abs())
}

The result of running the program is as follows.

1
2
3
4
5
50
5
50

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type iReadWriter interface {
    read()
    write()
}

type ReadWriter struct {
    content string
}

func (rw ReadWriter) read() {
    fmt.Printf("I am reading %s\n", rw.content)
}

func (rw *ReadWriter) write() {
    fmt.Printf("I am writing %s\n", rw.content)
}

func main() {
    var rw iReadWriter = &ReadWriter{"hello"}
    rw.read()
    rw.write()
}

The program runs with the following results.

1
2
I am reading hello
I am writing hello

But if you change the first statement of the main() function.

1
2
3
4
5
func main() {
    var rw iReadWriter = ReadWriter{"hello"}
    rw.read()
    rw.write()
}

It will error after running.

1
2
./prog.go:23:9: cannot use ReadWriter literal (type ReadWriter) as type iReadWriter in assignment:
    ReadWriter does not implement iReadWriter (write method has pointer receiver)

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.

  1. the ability of the method to modify the value pointed to by the receiver.
  2. 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type animal struct {
    name string
    age int
}

type cat struct {
    animal
    name string
}

func main() {
    c := cat{animal:animal{name:"animal",age:2},name:"cat"}
    fmt.Println(c)
    fmt.Println("name",c.name)
    fmt.Println("name",c.animal.name)
    fmt.Println("age",c.age)
    fmt.Println("age",c.animal.age)
}

The output of the program is as follows.

1
2
3
4
5
{{animal 2} cat}
name cat
name animal
age 2
age 2

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type Reader interface {
    read()
}

type Writer interface {
    write()
}

// Define the implementation classes of the above two interfaces
type MyReadWrite struct{}

func (mrw *MyReadWrite) read() {
    fmt.Println("MyReadWrite...read")
}

func (mrw *MyReadWrite) write() {
    fmt.Println("MyReadWrite...write")
}

// Define an interface that combines the above two interfaces.
type ReadWriter interface {
    Reader
    Writer
}

// The above interface is equivalent to
type ReadWriterV2 interface {
    read()
    write()
} 

// The ReadWriter and ReadWriterV2 interfaces are equivalent, so they can be assigned to each other
func interfaceTest() {
    mrw := &MyReadWrite{}
    // The mrw object implements the read() method and the write() method, so it can be assigned to ReadWriter and ReadWriterV2
    var rw1 ReadWriter = mrw
    rw1.read()
    rw1.write()

    fmt.Println("------")
    var rw2 ReadWriterV2 = mrw
    rw2.read()
    rw2.write()

    // Also, the ReadWriter and ReadWriterV2 interface objects can assign values to each other
    rw1 = rw2
    rw2 = rw1
}

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

  1. 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.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    type Job struct {
        Command string
        *log.Logger
    }
    
    func (job *Job)Start() {
        job.Log("starting now...")
        ...
        job.Log("started.")
    }
    

    In the above example, even though the combination is called as job.Log(...), the receiver of the Log method is still the log.Logger pointer, so it is not possible to access other member methods and variables of job in the Log method.

  2. 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. The Distance method has a parameter of type Point, but q is not a Point class, so even though q has the combined type Point, we have to explicitly select it, and trying to pass q 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.

    1
    2
    3
    
    func (p ColoredPoint) Distance(q Point) float64 {
        return p.Point.Distance(q)
    }
    

    When Point.Distance() is called by the above compiler-generated wrapper method, its receiver value is still p.Point, not p.

  3. 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.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    type Logger struct {
        Level int
    }
    
    type MyJob struct {
        *Logger
        Name string
        *log.Logger // duplicate field Logger
    }
    

    Both of the following points indirectly indicate that anonymous combinations are not inheritance.

    1. anonymous members have implicit names
    2. anonymous may conflict (duplex field)