Let's learn Go!

Basic composite types

In the previous chapter, we saw how to combine the traditional control flow idioms seen in chapter 3 to create blocks of reusable code, called functions. In this chapter, we’ll see how to combine the basic data types seen in chapter 2, in order to create complex data types.

The problem

Go comes with some primitive types: ints, floats, complex numbers, booleans, and strings. Fair enough, but you’ll notice as you start to write complex programs that you’ll need more advanced forms of data.

Let’s begin with an example.

In the previous chapter, we wrote the MAX(A, B int) function to find out the bigger value between two integers A, and B. Now, suppose that A, and B are actually the ages of persons whose names are respectively Tom and Bob. We need our program to output the name of the older person, and by how many years he is older.

Sure, we still can use the MAX(A, B int) function by giving it as input parameters, the ages of Tom and Bob. But it’d be nicer to write a function Older for people. And not for integers.

People! I said people! Thus the need to create a custom type to represent people, and this type will be used for the input parameters of the Older function we’d like to write.

Structs

In Go, as in C and other languages, we can declare new types that act as containers for attributes or fields of some other types. For example, we can create a custom type called person to represent the attributes of a person. In our example these attributes will be the person’s name and age.

digraph person {
    rankdir=LR;
    graph [bgcolor=transparent, resolution=96, fontsize="10" ];
    edge [arrowsize=.5, arrowtail="dot", color="#FF6600"];
    node [shape=record, fontsize=8, height=.1, penwidth=.4]
    person[label="<f0>Tom|<f1>25"];
    node[shape=plaintext];
    pName [label="P.name"];
    pAge [label="P.age"];
    pName->person:f0;
    pAge->person:f1;
}

A type like this is called a struct, which is short for structure.

How to declare a struct in Go?

1
2
3
4
type person struct {
    name string // Field name of type string.
    age int // Field age of type int.
}

See? It’s easy. A person struct is a container of two fields:

  • The person’s name: a field of type string.
  • The person’s age: a field of type int.

To declare a person variable we do exactly as we learned in chapter 2 for basic variable types.

Thus, writing things like:

1
2
3
4
5
6
type person struct {
    name string
    age int
}

var P person // P will be a variable of type person. How quaint!

We access and assign a struct’s fields (attributes) with the dot nottation. That is, if P is a variable of type person, then we access its fields like this:

1
2
3
P.name = "Tom" // Assign "Tom" to P's name field.
P.age = 25 // Set P's age to 25 (an int).
fmt.Println("The person's name is %s", P.name) // Access P's name field.

There’s even two short-form assignment statement syntaxes:

  1. By providing the values for each (and every) field in order:
1
2
3
//declare a variable P of type person and initialize its fields
//name to "Tom" and age to 25.
P := person{"Tom", 25}
  1. By using field:value initializers for any number of fields in any order:
1
2
3
4
//declare a variable P of type person and initialize its fields
//name to "Tom" and age to 25.
//Order, here, doesn't count since we specify the fields.
P := person{age:24, name:"Tom"}

Good. Let’s write our Older function that takes two input parameters of type person, and returns the older one and the difference in their ages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main
import "fmt"

// We declare our new type
type person struct {
    name string
    age int
}

// Return the older person of p1 and p2
//   and the difference in their ages.
func Older(p1, p2 person) (person, int) {
    if p1.age>p2.age { // Compare p1 and p2's ages
        return p1, p1.age-p2.age
    }
    return p2, p2.age-p1.age
}

func main() {
    var tom person

    tom.name, tom.age = "Tom", 18

    // Look how to declare and initialize easily.
    bob := person{age:25, name:"Bob"} //specify the fields and their values
    paul := person{"Paul", 43} //specify values of fields in their order

    tb_Older, tb_diff := Older(tom, bob)
    tp_Older, tp_diff := Older(tom, paul)
    bp_Older, bp_diff := Older(bob, paul)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
                tom.name, bob.name, tb_Older.name, tb_diff)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
                tom.name, paul.name, tp_Older.name, tp_diff)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
                bob.name, paul.name, bp_Older.name, bp_diff)
}

Output:

Of Tom and Bob, Bob is older by 7 years
Of Tom and Paul, Paul is older by 25 years
Of Bob and Paul, Paul is older by 18 years

And that’s about it! struct types are handy, easy to declare and to use!

Ready for another problem?

Now, suppose that we’d like to retrieve the older of, not 2, but 10 persons! Would you write a Older10 function that would take 10 input persons? Sure you can! But, seriously, that would be one ugly, very ugly function! Don’t write it, just picture it in your brain. Still not convinced? Ok, imagine a Older100 function with 100 input parameters! Convinced, now?

Ok, let’s see how to solve these kinds of problems using arrays.

Arrays

An array of size n is a block of n consecutive objects of the same type. That is a finite set of indexed objects, where you can enumerate them: object number 1, object number 2, etc...

In fact, in Go as in C and other languages, the indexing starts from 0, so you actually say: object number 0, object number 1... object number n-1

digraph array_s {
    rankdir=LR;
    graph [bgcolor=transparent, resolution=96, fontsize="10" ];
    edge [arrowsize=.5, arrowtail="dot", color="#555555"];
    node [shape=record, fontsize=8, height=.1, penwidth=.4]
    array[label="{<f0>A[0]|<f1>[A]1|<f2>A[2]|<f4>A[3]|...|<fn>A[n-1]}"];
}

Here’s how to declare an array of 10 ints, for example:

1
2
//declare an array A of 10 ints
var A [10]int

And here’s how to declare an array of 10 persons:

1
2
//declare an array A of 10 persons
var people [10]person

We can access the x th element of an array A, with the syntax: A[x]. Specifically we say: A[0], A[1], ..., A[n].

For example, the name of the 3 rd person is:

1
2
3
4
5
6
7
8
9
// Declare an array A of 10 persons:
var people [10]person

// Remember indices start from 0!
third_name := people[2].name //use the dot to access the third 'name'.

// Or more verbosely:
third_person := people[2]
thirdname := third_person.name

digraph array_s {
    //rankdir=LR;
    graph [bgcolor=transparent, resolution=96, fontsize="10" ];
    edge [arrowsize=.5, arrowhead="dot", color="#555555"];
    node [shape=record, fontsize=8, height=.1, penwidth=.4]
    people[label="<f0>people[0]|<f1>[people1]|<f2>people[2]|<f3>people[3]|...|<fn>people[n-1]"];
    p2[shape=circle, penwidth=1, color=crimson, fontcolor=black, label=<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" VALIGN="MIDDLE">
        <TR>
            <TD BGCOLOR="lightyellow">Tom</TD>
            <TD BGCOLOR="lightblue">25</TD>
        </TR>
    </TABLE>>]
    p2->people:f2 [label=" Zoom on people[2]", fontsize=6, color=crimson, fontcolor=crimson]
}

The idea now is to write a function Older10 that takes an array of 10 persons as its input, and returns the older person in this array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
import "fmt"

//Our struct
type person struct {
    name string
    age int
}

//return the older person in a group of 10 persons
func Older10(people [10]person) person {
    older := people[0] // The first one is the older for now.
    // Loop through the array and check if we could find an older person.
    for index := 1; index < 10; index++ { // We skipped the first element here.
        if people[index].age > older.age { // Current's persons age vs olderest so far.
            older = people[index] // If people[index] is older, replace the value of older.
        }
    }
    return older
}

func main() {
    // Declare an example array variable of 10 person called 'array'.
    var array [10]person

    // Initialize some of the elements of the array, the others are by
    // default set to person{"", 0}
    array[1] = person{"Paul", 23};
    array[2] = person{"Jim", 24};
    array[3] = person{"Sam", 84};
    array[4] = person{"Rob", 54};
    array[8] = person{"Karl", 19};

    older := Older10(array) // Call the function by passing it our array.

    fmt.Println("The older of the group is: ", older.name)
}

Output:

The older of the group is: Sam

We could have declared and initialized the variable array of the main() function above in a single shot like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Declare and initialize an array A of 10 person.
array := [10]person  {
    person{"", 0},
    person{"Paul", 23},
    person{"Jim", 24},
    person{"Sam", 84},
    person{"Rob", 54},
    person{"", 0},
    person{"", 0},
    person{"", 0},
    person{"Karl", 10},
    person{"", 0}}

This means that to initialize an array, you put its elements between two braces, and you separate them with commas.

We can even omit the size of the array, and Go will count the number of elements given in the initialization for us. So we could have written the above code as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Declare and initialize an array of 10 persons, but let the compiler guess the size.
array := [...]person  { // Substitute '...' instead of an integer size.
    person{"", 0},
    person{"Paul", 23},
    person{"Jim", 24},
    person{"Sam", 84},
    person{"Rob", 54},
    person{"", 0},
    person{"", 0},
    person{"", 0},
    person{"Karl", 10},
    person{"", 0}}

Specifically note the ellipsis [...].

Note

However, the size of an array is an important part of its definition. They don’t grow and they don’t shrink. You can’t, for example, have an array of 9 elements, and use it with a function that expects an array of 10 elements as an input. Simply because they are of different types. Remember this.

Some things to keep in mind:

  • Arrays are values. Assigning one array to another, copies all of the elements of the right hand array to the left hand array. That is: if A, and B are arrays of the same type, and we write: B = A, then B[0] == A[0], B[1] == A[1]... B[n-1] == A[n-1].
  • When passing an array to a function, it is copied. i.e. The function recieves a copy of that array, and not a reference (pointer) to it.
  • Go comes with a built-in function len that returns variables sizes. In the previous example: len(array) == 10.

Multi-dimensional arrays

You can think to yourself: “Hey! I want an array of arrays!”. Yes, that’s possible, and often used in practice.

We can declare a 2-dimensional array like this:

1
2
//declare and initialize an array of 2 arrays of 4 ints
double_array := [2][4]int {[4]int{1,2,3,4}, [4]int{5,6,7,8}}

This is an array of (2 arrays (of 4 int)). We can think of it as a matrix, or a table of two lines each made of 4 columns.

digraph array_2 {
    rankdir=LR;
    graph [bgcolor=transparent, resolution=96, fontsize="10" ];
    edge [arrowsize=.5, arrowtail="dot", color="#ff6600"];
    node [shape=record, fontsize=8, height=.1, penwidth=.4]
    array[label="{<f00>1|<f01>2|<f02>3|<f03>4}|{<f10>5|<f11>6|<f12>7|<f13>8}"];
    node[shape="plaintext", fixedsize="true"]
    exnode00 [label="A[0,0]"];
    exnode01 [label="A[0,1]"];
    exnode11 [label="A[1,1]"];
    exnode13 [label="A[1,3]"];
    exnode00->array:f00;
    exnode01->array:f01;
    exnode11->array:f11;
    exnode13->array:f13;
}

The previous declaration may be simplified, using the fact that the compiler can count arrays’ elements for us, like this:

1
2
//simplify the previous declaration, with the '...' syntax
double_array := [2][4]int {[...]int{1,2,3,4}, [...]int{5,6,7,8}}

Guess what? Since Go is about cleaner code, we can simplify this code even further:

1
2
//über simpifikation!
double_array := [2][4]int {{1,2,3,4}, {5,6,7,8}}

Cool, eh? Now, we can combine multiple fields of different types to create a struct and we can have arrays of, as many as we want, of objects of the same type, and pass it as a single block to our functions. But this is just the begining! In the next chapter, we will see how to create and use some advanced composite data types with pointers.