Although it is possible for us to write programs using only Go’s built-in data types, at some point it will become very tedious. Consider a program that interacts with a form like the code below.
package main
import (
"fmt"
"math"
)
func distance(x1, y1, x2, y2 float64) float64 {
a := x2 - x1
b := y2 - y1
return math.Sqrt(a*a + b*b)
}
func rectangleArea(x1, y1, x2, y2 float64) float64 {
l := distance(x1, y1, x1, y2)
w := distance(x1, y1, x2, y1)
return l * w
}
func circleArea(x, y, r float64) float64 {
return math.Pi * r * r
}
func main() {
var rx1, ry1 float64 = 0, 0
var rx2, ry2 float64 = 10, 10
var cx, cy, cr float64 = 0, 0, 5
fmt.Println(rectangleArea(rx1, ry1, rx2, ry2))
fmt.Println(circleArea(cx, cy, cr))
}
Keeping track of all the coordinates makes it difficult to see what the program is doing and will likely cause errors.
Structs
An easy way to make this program better is to use struct
. A struct
is a type that contains named fields. For example we can represent a circle like this:
type Circle struct {
x float64
y float64
r float64
}
The type
keyword introduces a new type. Followed by the type name (Circle), the keyword struct
to indicate that we are defining the type struct
and a list of fields in curly braces. Each field has a name and type. As with functions, we can collapse fields that have the same type:
type Circle struct {
x, y, r float64
}
Initialization
We can create instances of our new Circle type in various ways:
var c Circle
Like other data types, this will create a local variable Cirlce
which is set to zero by default. For struct
zero means each field is set to the corresponding zero value (0
for int, 0.0
for float, ""
for string, nil
for pointer, …) We also can use the new
function:
c := new(Circle)
It allocates memory for all fields, sets each to the value zero
and returns a pointer. (*Circle
) More often we use it to assign a value to each field
. We can do this in two ways. Like this:
c := Circle{x: 0, y: 0, r: 5}
Or we can ignore the field names if we know their order:
c := Circle{0, 0, 5}
Fields
We can access field
using the .
operator:
fmt.Println(c.x, c.y, c.r)
c.x = 10
c.y = 5
Let’s change the circleArea
function so that it uses Circle
:
func circleArea(c Circle) float64 {
return math.Pi * c.r*c.r
}
then inside the main function we give:
c := Circle{0, 0, 5}
fmt.Println(circleArea(c))
One thing to remember is that arguments are always copied in Go. If we try to change any of the fields
inside the circleArea
function, it will not change the original variable. Therefore, we usually write functions like this:
func circleArea(c *Circle) float64 {
return math.Pi * c.r*c.r
}
And change the main function to:
c := Circle{0, 0, 5}
fmt.Println(circleArea(&c))
##Methods
While this is better than the first version of this code, we can change it by using a special type of function known as method
:
func (c *Circle) area() float64 {
return math.Pi * c.r*c.r
}
Between the func keyword and the function name, we have added receiver
. receiver
is like a parameter it has a name and a type but by creating a function this way, we can call the function using the .
operator:
fmt.Println(c.area())
This is much easier to read, we no longer need the & operator (Go automatically knows to pass a pointer to a circle for this method) and since this function can only be used with circle
, we can rename the function to just area
.
Let’s do the same for the rectangle:
type Rectangle struct {
x1, y1, x2, y2 float64
}
func (r *Rectangle) area() float64 {
l := distance(r.x1, r.y1, r.x1, r.y2)
w := distance(r.x1, r.y1, r.x2, r.y1)
return l * w
}
and on main
we have:
r := Rectangle{0, 0, 10, 10}
fmt.Println(r.area())
Embedded Types
Field struct
usually represents interlocking. For example Circle
has a radius
. Suppose we have a person
struct:
type Person struct {
Name string
}
func (p *Person) Talk() {
fmt.Println("Hi, my name is", p.Name)
}
And we want to create a new Android
struct
. We can do this:
type Android struct {
Person Person
Model string
}
Go supports struct attachment like this by using embedded types. Also known as anonymous
fields, embedded types look like this:
type Android struct {
Person
Model string
}
We use a type (Person
) and don’t give it a name. When defined this way, the Person
struct can be accessed using the type name:
a := new(Android)
a.Person.Talk()
But we can also call any Person
method directly in Android
:
a := new(Android)
a.Talk()
Interfaces
Both in real life and in programming, relationships like this are commonplace. Go has a way of making these unintentional similarities explicit through a type known as Interface
. Here is an example of a Shape
interface:
type Shape interface {
area() float64
}
Like a struct
, an interface
is created using type
, followed by a name and interface
. But to define Shape
, we need a "method set"
. A method
set is a list of methods
that a type must have in order to “implement” an interface
.
func totalArea(shapes ...Shape) float64 {
var area float64
for _, s := range shapes {
area += s.area()
}
return area
}
we will call this function like this:
fmt.Println(totalArea(&c, &r))
Interface
can also be used as fields:
type MultiShape struct {
shapes []Shape
}
We can even change MultiShape
to Shape
by giving it an area
method:
func (m *MultiShape) area() float64 {
var area float64
for _, s := range m.shapes {
area += s.area()
}
return area
}
Now MultiShape
can contain Circles
, Rectangles
or even other MultiShapes
.
Below is a more complete program
package main
import (
"fmt"
"math"
)
type Circle struct {
x, y, r float64
}
func (c *Circle) area() float64 {
return math.Pi * c.r * c.r
}
type Rectangle struct {
l, w float64
}
func (r *Rectangle) area() float64 {
return r.l * r.w
}
type Shape interface {
area() float64
}
type MultiShape struct {
shapes []Shape
}
func (m *MultiShape) area() float64 {
var area float64
for _, s := range m.shapes {
area += s.area()
}
return area
}
func totalArea(shapes ...Shape) float64 {
var area float64
for _, s := range shapes {
area += s.area()
}
return area
}
func main() {
c := Circle{0, 0, 5}
r := Rectangle{3, 4}
fmt.Println(totalArea(&c, &r))
m := MultiShape{}
m.shapes = append(m.shapes, &c, &r)
fmt.Println(m.area())
}