25. Methods
A method is a function with a receiver — a value it's attached to. That's the whole idea. There's no class body to put it in; a method is declared at package level like any function, with one extra parameter in front of the name.
Declaring a method
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func main() {
r := Rectangle{Width: 3, Height: 4}
fmt.Println(r.Area())
fmt.Println(r.Perimeter())
}
The (r Rectangle) between func and the name is the receiver. Inside
the method, r is an ordinary parameter holding the value you called it on.
By convention the receiver name is short — one or two letters, usually the
first letter of the type — and it's the same name on every method of that
type. Go programmers don't use this or self.
A method is a function with different syntax
package main
import "fmt"
type Celsius float64
func (c Celsius) ToF() float64 {
return float64(c)*9/5 + 32
}
func toF(c Celsius) float64 {
return float64(c)*9/5 + 32
}
func main() {
temp := Celsius(100)
fmt.Println(temp.ToF())
fmt.Println(toF(temp))
}
Identical work, two spellings. Methods win because they're discoverable
(temp. shows you everything a Celsius can do), they let different types
share a method name, and — the big one — they're how a type satisfies an
interface.
Methods on any named type, not just structs
That example did something worth pausing on: Celsius is not a struct, it's
a named float64. You can define methods on any type you declare in your
package:
package main
import (
"fmt"
"strings"
)
type Tags []string
func (t Tags) Contains(want string) bool {
for _, tag := range t {
if tag == want {
return true
}
}
return false
}
func (t Tags) String() string {
return "[" + strings.Join(t, ", ") + "]"
}
type WordCount map[string]int
func (w WordCount) Total() int {
sum := 0
for _, n := range w {
sum += n
}
return sum
}
func main() {
t := Tags{"go", "backend", "wasm"}
fmt.Println(t.Contains("go"), t.Contains("rust"))
fmt.Println(t.String())
wc := WordCount{"a": 3, "b": 5}
fmt.Println(wc.Total())
}
Naming a slice or map type and hanging methods off it is very idiomatic Go — it turns a bag of data into something with vocabulary.
The one restriction: you can only define methods on types declared in your
own package. You can't add a method to string or to time.Time. If you
need to, declare type MyTime time.Time and add methods to that.
String() — the method fmt looks for
Give your type a String() string method and every fmt function will use
it automatically:
package main
import "fmt"
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
func main() {
p := Point{3, 4}
fmt.Println(p)
fmt.Printf("%v and %s\n", p, p)
fmt.Println([]Point{{1, 2}, {3, 4}})
}
Without it you'd see {3 4}. With it, your type controls how it prints
everywhere — including inside slices and maps.
This is your first interface, whether you noticed or not: fmt.Stringer is
defined as "any type with a String() string method", and Point satisfies
it just by having one. Nothing was declared, imported or registered. The next
module is entirely about that mechanism.
One warning: never call fmt.Sprintf("%v", p) on the receiver inside its
own String method — %v will call String again, forever. Format the
fields, not the whole value.
Methods and the value copy rule
A value receiver gets a copy, exactly like a normal parameter:
package main
import "fmt"
type Counter struct {
N int
}
func (c Counter) IncrementBroken() {
c.N++
}
func main() {
c := Counter{}
c.IncrementBroken()
c.IncrementBroken()
fmt.Println("after two increments:", c.N)
}
Still 0. IncrementBroken incremented its own copy and discarded it.
This is the most common surprise in the whole language for newcomers, and it's the entire subject of the next lesson: to modify the receiver, you need a pointer receiver.
Method values
Because a method is a function, you can grab one as a value:
package main
import "fmt"
type Greeter struct {
Name string
}
func (g Greeter) Hello() string {
return "Hello, " + g.Name
}
func main() {
g := Greeter{Name: "Ada"}
f := g.Hello
fmt.Println(f())
g.Name = "Grace"
fmt.Println(f())
fmt.Println(g.Hello())
}
f := g.Hello captures the receiver too — a copy of g as it was at
that moment. Changing g afterwards doesn't affect f. That's the value-copy
rule again, showing up in a new place.
Your turn
Give the Circle type an Area() method (π r², use math.Pi) and a
Describe() method that returns a string. The program should print exactly:
78.54
circle with radius 5.0
package main
import (
"fmt"
"math"
)
type Circle struct {
Radius float64
}
// add Area() float64 and Describe() string
func main() {
c := Circle{Radius: 5}
fmt.Printf("%.2f\n", c.Area())
fmt.Println(c.Describe())
}
package main
import (
"fmt"
"math"
)
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Describe() string {
return fmt.Sprintf("circle with radius %.1f", c.Radius)
}
func main() {
c := Circle{Radius: 5}
fmt.Printf("%.2f\n", c.Area())
fmt.Println(c.Describe())
}
Next: the other kind of receiver, and the one rule you must not break.