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.
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.
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 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]
}
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.
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 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 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
}
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)
}
}