PolarSPARC

Golang - Method Sets and Interfaces


Bhaskar S 02/01/2020


Go programming language (referred to as Golang) is an open source, strongly typed, high performance programming language from Google. One of the goals of Golang is to simplify the concept of object oriented programming by eliminating type hierarchy.

Method Sets is a core concept related to object oriented programming in Golang and we will cover a subtle nuance related to this concept.

Lets jump right into some examples to understand the concept of Method Sets.

Unlike other object oriented languages Golang does *NOT* have the concept of a class. Instead, Golang leverages structure types.

A structure (or composite type) is defined using the keyword struct and is a set of fields, where each field has an associated name and a data type.

The following is a simple example demonstrating the use of a struct:

Listing.1
package main

import "fmt"

type contact struct {
    firstName string
    lastName string
    email string
}

func main() {
    alice := contact{firstName: "Alice", lastName: "Earthling", email: "alice@earth.pl"}

    fmt.Printf("Alice (%T) => %v\n", alice, alice)
    fmt.Printf("Alice (%T) => %+v\n", alice, alice)

    fmt.Printf("Alice (First Name): %s\n", alice.firstName)
    fmt.Printf("Alice (Last Name): %s\n", alice.lastName)
    fmt.Printf("Alice (Email): %s\n", alice.email)
}

Executing the program from Listing.1 will generate the following output:

Output.1

Alice (main.contact) => {Alice Earthling alice@earth.pl}
Alice (main.contact) => {firstName:Alice lastName:Earthling email:alice@earth.pl}
Alice (First Name): Alice
Alice (Last Name): Earthling
Alice (Email): alice@earth.pl

A function is defined using the keyword func to implement an operation. In Listing.1 above, main is a function.

A method is nothing more than a function except that it attaches to a data type via a type parameter (referred to as a receiver type) that appears before the function name.

The following example demonstrates object oriented style by attaching functions (methods) to a structure:

Listing.2
package main

import "fmt"

type contact struct {
    firstName string
    lastName  string
    email     string
}

func (c contact) getFirstName() string {
    return c.firstName
}

func (c contact) getLastName() string {
    return c.lastName
}

func (c contact) getEmail() string {
    return c.email
}

func main() {
    bob := contact{firstName: "Bob", lastName: "Martian", email: "bob@mars.pl"}

    fmt.Printf("Bob (%T) => %v\n", bob, bob)
    fmt.Printf("Bob (First Name): %s\n", bob.getFirstName())
    fmt.Printf("Bob (Last Name): %s\n", bob.getLastName())
    fmt.Printf("Bob (Email): %s\n", bob.getEmail())
}

Executing the program from Listing.2 will generate the following output:

Output.2

Bob (main.contact) => {Bob Martian bob@mars.pl}
Bob (First Name): Bob
Bob (Last Name): Martian
Bob (Email): bob@mars.pl

In Listing.2 above, the parameter (c contact) after the func keyword is the receiver type. This is what binds the functions (referred to as methods) getFirstName, getLastName, and getEmail to the structure type contact.

The collection of methods attached to a structure are referred to as the Method Set.

⛔ ATTENTION ⛔

The Method Set of any type T consists of all methods declared with receiver type T.

By default, Golang uses the pass-by-value semantics when passing value(s) to a function. Any updates to the passed value within the function have no affect on the original value. This is where a pointer type comes in handy.

For a given data type T, a pointer to T (represented as *T) is a data type that points to the memory address of T.

The following example demonstrates the use of pointer reveiver type for attaching method(s) to a structure:

Listing.3
package main

import "fmt"

type contact struct {
    firstName string
    lastName  string
    email     string
}

func (c contact) getFirstName() string {
    return c.firstName
}

func (c contact) getLastName() string {
    return c.lastName
}

func (c contact) getEmail() string {
    return c.email
}

func (c *contact) setEmail(email string) {
    c.email = email
}

func main() {
    bob := contact{firstName: "Bob", lastName: "Martian", email: "bob@mars.pl"}

    fmt.Printf("Bob (%T) => %v\n", bob, bob)
    fmt.Printf("Bob (First Name): %s\n", bob.getFirstName())
    fmt.Printf("Bob (Last Name): %s\n", bob.getLastName())
    fmt.Printf("Bob (Email): %s\n", bob.getEmail())

    bob.setEmail("bob@mars.com")

    fmt.Printf("Bob (Updated Email): %s\n", bob.getEmail())

    charlie := &contact{firstName: "Charlie", lastName: "Plutoid", email: "charlie@pluto.po"}

    fmt.Printf("Charlie (%T) => %v\n", charlie, charlie)
    fmt.Printf("Charlie (First Name): %s\n", charlie.getFirstName())
    fmt.Printf("Charlie (Last Name): %s\n", charlie.getLastName())
    fmt.Printf("Charlie (Email): %s\n", charlie.getEmail())

    charlie.setEmail("charlie@plutoid.org")

    fmt.Printf("Charlie (Updated Email): %s\n", charlie.getEmail())
}

Executing the program from Listing.3 will generate the following output:

Output.3

Bob (main.contact) => {Bob Martian bob@mars.pl}
Bob (First Name): Bob
Bob (Last Name): Martian
Bob (Email): bob@mars.pl
Bob (Updated Email): bob@mars.com
Charlie (*main.contact) => &{Charlie Plutoid charlie@pluto.po}
Charlie (First Name): Charlie
Charlie (Last Name): Plutoid
Charlie (Email): charlie@pluto.po
Charlie (Updated Email): charlie@plutoid.org

In Listing.3 above, notice the use of the pointer receiver type (c *contact) for the method setEmail.

Also, notice the variable charlie is a pointer type (using ampersand when creating a contact value - &contact).

⛔ ATTENTION ⛔

The Method Set of any type T or *T is the set of all methods declared with receiver type T and *T (that is, it also contains the Method Set of T).

An interface type defines the signatures for a collection of methods that can be invoked on any type that conforms to the interface type. In other words, an interface defines a collection of common methods (across different types) and have no implementation details, but just their signatures.

The collection of methods attached to an interface are referred to as the Method Set.

The following example demonstrates the use of an interface:

Listing.4
package main

import "fmt"

type contact struct {
    firstName string
    lastName  string
    email     string
}

func (c contact) getFirstName() string {
    return c.firstName
}

func (c contact) getLastName() string {
    return c.lastName
}

func (c contact) getEmail() string {
    return c.email
}

func (c contact) print(s string) {
    fmt.Printf("%s => [%s|%s|%s]\n", s, c.firstName, c.lastName, c.email)
}

func (c *contact) setEmail(email string) {
    c.email = email
}

type demographics struct {
    location string
    gender string
}

func (d demographics) print(s string) {
    fmt.Printf("%s => [%s|%s]\n", s, d.location, d.gender)
}

type printer interface {
    print(s string)
}

func display(s string, p printer) {
    p.print(s)
}

func main() {
    alice := contact{firstName: "Alice", lastName: "Earthling", email: "alice@earth.pl"}

    fmt.Printf("Alice (%T) => %v\n", alice, alice)
    fmt.Printf("Alice (firstName): %s\n", alice.getFirstName())
    fmt.Printf("Alice (lastName): %s\n", alice.getLastName())
    fmt.Printf("Alice (email): %s\n", alice.getEmail())

    alice.setEmail("alice@earthling.com")

    fmt.Printf("Alice (email): %s\n", alice.getEmail())

    alice.print("contact(alice)")

    ad := demographics{location: "Earth", gender: "Human"}

    ad.print("demographics(alice)")

    display("display.contact(alice)", alice)
    display("display.demographics(alice)", ad)

    bob := &contact{firstName: "Bob", lastName: "Martian", email: "bob@mars.pl"}

    fmt.Printf("Bob (%T) => %v\n", bob, *bob)
    fmt.Printf("Bob (firstName): %s\n", bob.getFirstName())

    bob.setEmail("bob@mars.io")

    fmt.Printf("Bob (email): %s\n", bob.getEmail())

    bob.print("contact(bob)")

    bd := demographics{location: "Mars", gender: "Martian"}

    bd.print("demographics(bob)")

    display("display.contact(bob)", bob)
    display("display.demographics(bob)", bd)
}

Executing the program from Listing.4 will generate the following output:

Output.4

Alice (main.contact) => {Alice Earthling alice@earth.pl}
Alice (firstName): Alice
Alice (lastName): Earthling
Alice (email): alice@earth.pl
Alice (email): alice@earthling.com
contact(alice) => [Alice|Earthling|alice@earthling.com]
demographics(alice) => [Earth|Human]
display.contact(alice) => [Alice|Earthling|alice@earthling.com]
display.demographics(alice) => [Earth|Human]
Bob (*main.contact) => {Bob Martian bob@mars.pl}
Bob (firstName): Bob
Bob (email): bob@mars.io
contact(bob) => [Bob|Martian|bob@mars.io]
demographics(bob) => [Mars|Martian]
display.contact(bob) => [Bob|Martian|bob@mars.io]
display.demographics(bob) => [Mars|Martian]

In Listing.4 above, there are two different struct types - contact and demographics. Both these structures have a common method print(s string) attached to them.

Also, notice the definition of an interface type printer which specifies a method with the signature print(s string).

Since both the struct types contact and demographics implement the method signature print(s string), they implicitly are of the interface type printer.

⛔ ATTENTION ⛔

A variable of interface type can store a value of any type with a Method Set that is any superset of the interface. Such a type is said to implement the interface.

Now for the interesting question - what will happen if we change the receiver types for the method print(s string) to their respective pointer types ???

The following code is the same as in Listing.4 above, except that the receiver type for the two print(s string) methods have been changed to pointer receiver type:

Listing.5
package main

import "fmt"

type contact struct {
    firstName string
    lastName  string
    email     string
}

func (c contact) getFirstName() string {
    return c.firstName
}

func (c contact) getLastName() string {
    return c.lastName
}

func (c contact) getEmail() string {
    return c.email
}

func (c *contact) print(s string) {
    fmt.Printf("%s => [%s|%s|%s]\n", s, c.firstName, c.lastName, c.email)
}

func (c *contact) setEmail(email string) {
    c.email = email
}

type demographics struct {
    location string
    gender string
}

func (d *demographics) print(s string) {
    fmt.Printf("%s => [%s|%s]\n", s, d.location, d.gender)
}

type printer interface {
    print(s string)
}

func display(s string, p printer) {
    p.print(s)
}

func main() {
    alice := contact{firstName: "Alice", lastName: "Earthling", email: "alice@earth.pl"}

    fmt.Printf("Alice (%T) => %v\n", alice, alice)
    fmt.Printf("Alice (firstName): %s\n", alice.getFirstName())
    fmt.Printf("Alice (lastName): %s\n", alice.getLastName())
    fmt.Printf("Alice (email): %s\n", alice.getEmail())

    alice.setEmail("alice@earthling.com")

    fmt.Printf("Alice (email): %s\n", alice.getEmail())

    alice.print("contact(alice)")

    ad := demographics{location: "Earth", gender: "Human"}

    ad.print("demographics(alice)")

    display("display.contact(alice)", alice) // <-- [1] ERROR
    display("display.demographics(alice)", ad) // <-- [2] ERROR

    bob := &contact{firstName: "Bob", lastName: "Martian", email: "bob@mars.pl"}

    fmt.Printf("Bob (%T) => %v\n", bob, *bob)
    fmt.Printf("Bob (firstName): %s\n", bob.getFirstName())

    bob.setEmail("bob@mars.io")

    fmt.Printf("Bob (email): %s\n", bob.getEmail())

    bob.print("contact(bob)")

    bd := demographics{location: "Mars", gender: "Martian"}

    bd.print("demographics(bob)")

    display("display.contact(bob)", bob)
    display("display.demographics(bob)", bd) // <-- [3] ERROR
}

In Listing.5 above will not compile. For the line with the comment [1] ERROR, we will get the following error:

ERROR

Cannot use 'alice' (type contact) as type printer Type does not implement 'printer' as 'print' method has a pointer receiver

Similarly, for the lines with the respective comments [2] ERROR and [3] ERROR, we will get the following errors:

ERROR

Cannot use 'ad' (type demographics) as type printer Type does not implement 'printer' as 'print' method has a pointer receiver
--- AND ---
Cannot use 'bd' (type demographics) as type printer Type does not implement 'printer' as 'print' method has a pointer receiver

When a method is invoked on an interface, it MUST either have an identical receiver type or it must be directly discernible from the concrete type. Methods with receiver type T CAN be called with *T values because they can be dereferenced. Methods with receiver type *T CANNOT be called with values of type T because the value stored inside an interface has no address.


References

The Go Programming Language Specification

Golang Interfaces



© PolarSPARC