Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Latest commit

 

History

History
executable file
·
375 lines (276 loc) · 9.82 KB

02.4.md

File metadata and controls

executable file
·
375 lines (276 loc) · 9.82 KB

02.4 - Methods and interfaces

Methods

Methods can be defined for types (e.g. structs). A method is a function with a special receiver, receiver is the type that a method is defined for.

Create methods for slices

Let's say want to create a method for a string array that prints the members. First problem is that we cannot create a method for type []string because it's an unnamed type and they cannot be method receivers. The trick is to declare a new type for []string and then define the method for that type.

// 02.4-01-method1.go
package main

import "fmt"

// Create a new type for []string
type StringSlice []string

// Define the method for StringSlice
func (x StringSlice) PrintSlice() {
    for _, v := range x {
        fmt.Println(v)
    }
}

func main() {

    // Create an array of strings with 3 members
    characters := [3]string{"Ender", "Petra", "Mazer"}

    // Create a StringSlice
    var allMembers StringSlice = characters[0:3]

    // Now we can call the method on it
    allMembers.PrintSlice()

    // Ender
    // Petra
    // Mazer

    // allMembers.PrintSlice()
    // allMembers.PrintSlice undefined (type []string has no field or method PrintSlice)
}

Note that we cannot call PrintSlice() on []string although they are essentially the same type.

Value vs. pointer receivers

In the previous example we created a value receiver. In methods with value receivers, the method gets a copy of the object and the initial object is not modified.

We can also designate a pointer as receiver. In this case, any changes on the pointer inside the method are reflected on the referenced object.

Pointer receivers are usually used when a method changes the object or when it's called on a large struct. Because value receivers copy the whole object, a large struct will consume a lot of memory but pointer receivers do not have this overhead.

Pointer Receivers

Pointer receivers get a pointer instead of a value but can modify the referenced object.

In the following code, Tuple's fields will be modified by ModifyTuplePointer() but not by ModifyTupleValue().

However, this is not the case for slices (e.g. IntSlice in the code). Both value and pointer receivers modify the slice.

Pointer receivers are more efficient because they do not copy the original object.

All methods for one type should either have value receivers or pointer receivers, do not mix and match like the code below :).

// 02.4-02-method2.go
package main

import "fmt"

// Tuple type
type Tuple struct {
    A, B int
}

// Should not change the value of the object as it works on a copy of it
func (x Tuple) ModifyTupleValue() {
    x.A = 2
    x.B = 2
}

// Should change the value of the object
func (x *Tuple) ModifyTuplePointer() {
    x.A = 3
    x.B = 3
}

type IntSlice []int

func (x IntSlice) PrintSlice() {
    fmt.Println(x)
}

// Modifies the IntSlice although it's by value
func (x IntSlice) ModifySliceValue() {
    x[0] = 1
}

// Modifies the IntSlice
func (x *IntSlice) ModifySlicePointer() {
    (*x)[0] = 2
}

func main() {

    tup := Tuple{1, 1}

    tup.ModifyTupleValue()
    fmt.Println(tup) // {1 1} - Does not change

    tup.ModifyTuplePointer()
    fmt.Println(tup) // {3 3} - Modified by pointer receiver

    var slice1 IntSlice = make([]int, 5)
    slice1.PrintSlice() // [0 0 0 0 0]

    slice1.ModifySliceValue()
    slice1.PrintSlice() // [1 0 0 0 0]

    slice1.ModifySlicePointer()
    slice1.PrintSlice() // [2 0 0 0 0]
}

When to use methods vs. functions

Methods are special functions. In general use methods when:

  • The output is based on the state of the receiver. Functions do not care about states.
  • The receiver must to be modified.
  • The method and receiver are logically connected.

Interfaces

An interface is not Generics! An interface can be one type of a set of types that implement a set of specific methods.

For example we will define an interface which has the method MyPrint(). If we define and implement MyPrint() for type B, a variable of type B can be assigned to an interface of that type.

// 02.4-03-interface1.go
package main

import "fmt"

// Define new interface
type MyPrinter interface {
    MyPrint()
}

// Define a type for int
type MyInt int

// Define MyPrint() for MyInt
func (i MyInt) MyPrint() {
    fmt.Println(i)
}

// Define a type for float64
type MyFloat float64

// Define MyPrint() for MyFloat
func (f MyFloat) MyPrint() {
    fmt.Println(f)
}

func main() {

    // Define interface
    var interface1 MyPrinter

    f1 := MyFloat(1.2345)
    // Assign a float to interface
    interface1 = f1
    // Call MyPrint() on interface
    interface1.MyPrint() // 1.2345

    i1 := MyInt(10)
    // Assign an int to interface
    interface1 = i1
    // Call MyPrint() on interface
    interface1.MyPrint() // 10
}

Empty Interface is interface {} and can hold any type. We are going to use empty interfaces a lot in functions that handle unknown types.

// 02.4-04-interface2.go
package main

import "fmt"

var emptyInterface interface{}

type Tuple struct {
    A, B int
}

func main() {

    // Use int
    int1 := 10
    emptyInterface = int1
    fmt.Println(emptyInterface) // 10

    // Use float
    float1 := 1.2345
    emptyInterface = float1
    fmt.Println(emptyInterface) // 1.2345

    // Use custom struct
    tuple1 := Tuple{5, 5}
    emptyInterface = tuple1
    fmt.Println(emptyInterface) // {5 5}
}

We can access the value inside the interface after casting. But if the interface does not contain a float, it will trigger a panic:

  • myFloat := myInterface(float64)

In order to prevent panic we can check the error returned by casting and handle the error.

  • myFloat, ok := myInterface(float64).

If the interface has a float, ok will be true and otherwise false.

// 02.4-05-interface3.go
package main

import "fmt"

func main() {
    var interface1 interface{} = 1234.5

    // Only print f1 if cast was successful
    if f1, ok := interface1.(float64); ok {
        fmt.Println("Float")
        fmt.Println(f1) // 1234.5
    }

    f2 := interface1.(float64)
    fmt.Println(f2) // 1234.5 No panic but not recommended

    // This will trigger a panic
    // i1 = interface1.(int)

    i2, ok := interface1.(int) // No panic
    fmt.Println(i2, ok)        // 0 false
}

Type switch

Type switches are usually used inside functions that accept empty interfaces. They are used to determine the type of data that inside the interface and act accordingly.

A type switch is a switch on interface.(type) and some cases.

// 02.4-06-typeswitch.go
package main

import "fmt"

func printType(i interface{}) {
    // Do a type switch on interface
    switch val := i.(type) {
    // If an int is passed
    case int:
        fmt.Println("int")
    case string:
        fmt.Println("string")
    case float64:
        fmt.Println("float64")
    default:
        fmt.Println("Other:", val)
    }
}

func main() {
    printType(10)      // int
    printType("Hello") // string
    printType(156.32)  // float64
    printType(nil)     // Other: <nil>
    printType(false)   // Other: false
}

Stringers

Stringers overload print methods. A Stringer is a method named String() that returns a string and is defined with a specific type as receiver (usually a struct).

type Stringer interface {
    String() string
}

After the definition, if any Print function is called on the struct, the Stringer will be invoked instead. For example if a struct is printed with %v format string verb (we will see later that this verb prints the value of an object), Stringer is invoked.

// 02.4-07-stringer1.go
package main

import "fmt"

// Define a struct
type Tuple struct {
    A, B int
}

// Create a Stringer for Tuple
func (t Tuple) String() string {
    // Sprintf is similar to the equivalent in C
    return fmt.Sprintf("A: %d, B: %d", t.A, t.B)
}

func main() {

    tuple1 := Tuple{10, 10}
    tuple2 := Tuple{20, 20}
    fmt.Println(tuple1) // A: 10, B: 10
    fmt.Println(tuple2) // A: 20, B: 20
}

Solution to the Stringers exercise

Make the IPAddr type implement fmt.Stringer to print the address as a dotted quad. For instance, IPAddr{1, 2, 3, 4} should print as 1.2.3.4.

// 02.4-08-stringer2.go
package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (ip IPAddr) String() string {
    return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
}

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}