Go: Part 4
Methods
Go does not have classes. However, you can define methods on types.
A method is a function with a special receiver argument
. The receiver appears in its own argument list between the func keyword and the method name.
In the following example, the Hyp method has a receiver of type Vertex
named v. This v can be considered equivalent to using this
in other OO languages.
type Vertex struct {
X, Y float64
}
func (v Vertex) HypMethod() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// Function version of the above method
func HypFunction(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func methodTest1() {
h := Vertex{3, 4}
fmt.Println(h.HypMethod()) // prints 5
// equivalently, using a function
fmt.Println(HypFunction(h)) // prints 5
}
You can declare a method on non-struct types too. In the following example we see a numeric type MyFloat with an Abs method. You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int
).
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func methodTest2() {
f := MyFloat(-math.Sqrt2) // -math.Sqrt2 = -1.4142135623730951
fmt.Println(f.Abs()) // prints 1.4142135623730951
}
You can declare methods with pointer receivers. This means the receiver type has the literal syntax *T
for some type T
. (Also, T
cannot itself be a pointer such as *int
.)
In the following example, the Scale method here is defined on *Vertex
. Methods with pointer receivers can modify the value to which the receiver points (as Scale method does here). Since methods often need to modify their receiver, pointer receivers are more common than value receivers.
func (v *Vertex) ScaleMethod(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
// Function version of the above method
func ScaleFunction(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func methodTest3() {
v := Vertex{3, 4}
v.ScaleMethod(10) // v gets updated
// equivalently, using a function
ScaleFunction(&v, 10)
}
Nuances when using pointers with methods and functions:
var v Vertex
ScaleFunction(v, 5) // Compile error!
ScaleFunction(&v, 5) // OK
var v Vertex
v.ScaleMethod(5) // OK
p := &v
p.ScaleMethod(10) // OK
For the statement v.ScaleMethod(5)
, even though v
is a value and not a pointer, it is passed as a pointer since the ScaleMethod
has a pointer receiver.
That is, as a convenience, Go interprets the statement v.Scale(5)
as (&v).Scale(5)
Nuances when using values with methods and functions:
var v Vertex
fmt.Println(HypFunction(v)) // OK
fmt.Println(HypFunction(&v)) // Compile error!
var v Vertex
fmt.Println(v.HypMethod()) // OK
p := &v
fmt.Println(p.HypMethod()) // OK
Methods with value receivers can also be called on pointers. That is, the method call p.HypMethod()
is interpreted by Go as (*p).HypMethod()
. Since the method uses a value receiver, it still cannot modify the values pointed by p
. It is essentially call by value, even though you are calling it on a pointer. There are two reasons to use a pointer receiver:
- The first is so that the method can modify the value that its receiver points to.
- The second is to avoid copying the value on each method call. This can be more efficient if the receiver is a large struct, for example.
As good practice, all methods on a given type should have either value or pointer receivers, but not a mixture of both.
Interfaces
An interface type is defined as a set of method signatures. A variable of interface type can hold any value that has implementations for those methods.
type Abser interface {
Abs() float64
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func interfaceTest1() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f
a = &v
a = v // Error
fmt.Println(a.Abs())
}
In the above example, there is an an Abs method defined with MyFloat
receiver argument, hence MyFloat type implements Abser, so assigning a = f
succeeds. Similarly, there is an Abs method defined with *Vertex
receiver argument, hence *Vertex
type implements Abser, so assigning a = &v
succeeds. However, there is no method implementation for Abs defined with Vertex
receiver argument (even though there is one for its pointer), so assigning a = v
throws an error.
Under the hood, interface values can be thought of as a tuple of a value and a concrete type: (value, type)
. An interface value holds a value
of a specific underlying concrete type
. Calling a method on an interface value executes the method of the same name on its underlying type
.
a = f
fmt.Printf("(%v, %T)\n", a, a) // prints (-1.4142135623730951, main.MyFloat)
Interface values with nil underlying values
If the concrete value
inside the interface itself is nil, the method will be called with a nil receiver. In some languages this would trigger a null pointer exception, but in Go it is common to write methods that gracefully handle being called with a nil receiver. Note that an interface value that holds a nil concrete value is itself non-nil.
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
func interfaceTest2() {
var i I
var t *T
i = t
describe(i) // prints (<nil>, *main.T)
i.M() // prints <nil>
i = &T{"hello"}
describe(i) // prints (&{hello}, *main.T)
i.M() // prints hello
}
On the other hand, a nil interface value holds neither value
nor concrete type
. Calling a method on a nil interface is a run-time error because there is no type inside the interface tuple to indicate which concrete method to call.
var i I
describe(i) // prints (<nil>, <nil>)
i.M() // runtime error
Empty Interface
The interface type that specifies zero methods is known as the empty interface: interface{}
. An empty interface may hold values of any type (since every type implements at least zero methods). Empty interfaces are used by code that handles values of unknown type. For example, fmt.Print
takes any number of arguments of type interface{}. It’s function signature is: func Print(a ...interface{}) (n int, err error)
var i interface{}
fmt.Printf("(%v, %T)\n", i, i) // prints (<nil>, <nil>)
i = 42
fmt.Printf("(%v, %T)\n", i, i) // prints (42, int)
i = "hello"
fmt.Printf("(%v, %T)\n", i, i) // prints (hello, string)
Type assertions
A type assertion provides access to an interface value’s underlying concrete value: t := i.(T)
. If i does not hold a value of type T, the statement will trigger a panic (A special type of error, which stops regular flow of execution, executes any deferred function calls, and returns to parent, where the panic can be handled using recover
). To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean
value that reports whether the assertion succeeded. t, ok := i.(T)
. If i holds a value of type T
, then t will take that value and ok will be true
. If not, ok will be false
and t will be the zero value of type T
, and no panic occurs.
var i interface{} = "hello"
s := i.(string)
fmt.Println(s) // prints hello
s, ok := i.(string)
fmt.Println(s, ok) // prints hello true
f, ok := i.(float64)
fmt.Println(f, ok) // prints 0 false
f = i.(float64) // triggers a panic
fmt.Println(f) // doesn't execute
Type switch
A type switch is a construct that permits several type assertions in series. A type switch is like a regular switch statement, but the cases in a type switch specify types (not values), and those types are compared against the type of the value held by the given interface value.
switch v := i.(type) { // type is a keyword
case T:
// here v is the underlying value of i, which is a value of type T
case S:
// here v is the underlying value of i, which is a value of type S
default:
// None of the cases had the actual type; here v has the same type as the underlying type of i
}
Applications of Interface: Stringers
type Stringer interface {
String() string
}
One of the most ubiquitous interfaces is Stringer
defined by the fmt
package. A Stringer
is a type that can describe itself as a string. The fmt
package (and many others) look for this interface to print values. It’s similar to toString()
that’s present in all Java objects.
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)\n", p.Name, p.Age)
}
func stringerTest() {
a := Person{"Harvey Dent", 42}
z := Person{"Batman", 81}
fmt.Println(a, z)
// prints: Harvey Dent (42 years)
// Batman (81 years)
}
Applications of Interface: Errors
Go programs express error state with error
values. The error
type is a built-in interface similar to fmt.Stringer
:
type error interface {
Error() string
}
As with fmt.Stringer
, the fmt
package looks for the error
interface when printing values.
import (
"fmt"
"time"
)
type MyError struct {
What string
When time.Time
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s, at %v",
e.What, e.When)
}
// We can return a *MyError here since we have previously declared a method for *MyError, therefore implementing the `error` interface
func run() error {
return &MyError{
"it didn't work",
time.Now(),
}
}
func ErrorTest() {
if err := run(); err != nil {
fmt.Println(err) // prints: it didn't work, at 2020-10-12 21:05:04 +0000 UTC m=+0.000000001
}
}
Applications of Interface: Readers
The io
package specifies the io.Reader
interface, which represents the read end of a stream of data. The Go standard library contains many implementations of these interfaces, including files, network connections, compressors, ciphers, and others. The io.Reader
interface has a Read method:
func (T) Read(b []byte) (n int, err error)
Read populates the given byte slice with data and returns the number of bytes populated and an error value. It returns an io.EOF
error when the stream ends. The following code creates a strings.Reader
and consumes its output 4 bytes at a time.
import (
"fmt"
"io"
"strings"
)
func readerTest() {
r := strings.NewReader("Hello")
b := make([]byte, 4)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
// prints:
// n = 4 err = <nil> b = [72 101 108 108]
// b[:n] = "Hell"
// n = 1 err = <nil> b = [111 101 108 108]
// b[:n] = "o"
// n = 0 err = EOF b = [111 101 108 108]
// b[:n] = ""
}
Here is an example of a custom Reader, which gives an infinite stream of ‘A’s.
type MyReader struct{}
func (r MyReader) Read(b []byte) (int, error) {
for i,_ := range b {
b[i] = byte('A')
}
return len(b), nil
}